diff --git a/.github/AUTOMATIC_RELEASES.md b/.github/AUTOMATIC_RELEASES.md new file mode 100644 index 0000000..312fc41 --- /dev/null +++ b/.github/AUTOMATIC_RELEASES.md @@ -0,0 +1,226 @@ +# 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..7091d70 --- /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/cYauqJfnNK](https://discord.gg/cYauqJfnNK) + +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..ebfebae 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 @@ -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 ``` @@ -283,14 +291,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/cYauqJfnNK) +- 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..b5b3593 --- /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..96f02af --- /dev/null +++ b/.github/VERSIONING.md @@ -0,0 +1,157 @@ +# 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..eaf50ec --- /dev/null +++ b/.github/scripts/analyze-commits.js @@ -0,0 +1,269 @@ +#!/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..400afed --- /dev/null +++ b/.github/scripts/get-version.js @@ -0,0 +1,163 @@ +#!/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..03c2be3 --- /dev/null +++ b/.github/scripts/update-trusted-hosts.js @@ -0,0 +1,233 @@ +#!/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, + "..", + "..", + "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); + }); +} + +/** + * 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..ad3a412 --- /dev/null +++ b/.github/scripts/validate-trusted-hosts.js @@ -0,0 +1,109 @@ +#!/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 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", +); + +try { + // Load files + 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}`); + }); + 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..e19f055 --- /dev/null +++ b/.github/scripts/validate-tsconfig.js @@ -0,0 +1,115 @@ +#!/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/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/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..2021ed8 --- /dev/null +++ b/.github/workflows/knip-ci.yml @@ -0,0 +1,46 @@ +name: Knip Validation +on: + push: + branches: [master, develop] + pull_request: + +jobs: + knip: + runs-on: ubuntu-latest + steps: + - 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 + + # HIGH CONFIDENCE - Hard fail on unused dependencies + - name: Validate Dependency Usage + run: bunx knip:deps + + # MEDIUM CONFIDENCE - Report only (warning) + - name: Validate File Usage + run: bunx knip:files + continue-on-error: true + + # LOW CONFIDENCE - Report only (warning) + - name: Validate Export Usage + run: bunx knip:exports + continue-on-error: true + + # LOW CONFIDENCE - Report only (warning) + - name: Validate Production Dependencies + run: bunx knip:prod + continue-on-error: true + + - name: Notify success + run: echo "Knip validation completed!" diff --git a/.github/workflows/knip-report.yml b/.github/workflows/knip-report.yml new file mode 100644 index 0000000..af587b8 --- /dev/null +++ b/.github/workflows/knip-report.yml @@ -0,0 +1,53 @@ +name: Knip Report + +on: + pull_request: + +jobs: + knip-report: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - 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 + + - name: Generate Knip Report + id: knip-report + run: | + { + echo "## 📋 Knip Analysis Report" + echo "" + echo "### Exports" + bunx knip:exports 2>&1 || true + echo "" + echo "### Files" + bunx knip:files 2>&1 || true + echo "" + echo "### Production" + bunx knip:prod 2>&1 || true + } >> $GITHUB_OUTPUT + + - name: Comment on PR + if: github.event_name == 'pull_request' + 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: "## 📋 Knip Code Quality Analysis\n\nThis report identifies potentially unused exports, dependencies, and files. **Note:** These are warnings only and may contain false positives, especially for:\n- MDX/documentation components\n- Dynamic imports\n- Framework magic (Next.js, Fumadocs)\n\n### Summary\n- Only **hard fails** occur for unused dependencies (knip:deps)\n- All other checks are informational warnings\n\nTo run locally:\n- `bun run knip:deps` - Check unused dependencies (hard fail)\n- `bun run knip:exports` - Check unused exports (warning)\n- `bun run knip:files` - Check unused files (warning)\n- `bun run knip:prod` - Check production deps (warning)\n\nSee [Knip Documentation](https://knip.dev) for more details." + }) 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..2d3dc4d --- /dev/null +++ b/.github/workflows/tsconfig-validation.yml @@ -0,0 +1,44 @@ +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\n\nThis pull request attempts to **remove or modify** critical tsconfig.json configurations. This is not allowed as it will break the site.\n\n### ✅ What's Allowed:\n- **Adding** new compiler options\n- **Adding** new path aliases\n- **Adding** new include/exclude patterns\n\n### ❌ What's Not Allowed:\n- Removing existing compiler options\n- Modifying existing compiler option values\n- Removing path aliases\n- Modifying path alias mappings\n- Removing include/exclude patterns\n\nPlease 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..9b17557 --- /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..d3b5dc7 --- /dev/null +++ b/.github/workflows/validate-providers.yml @@ -0,0 +1,166 @@ +name: Validate Provider Files + +on: + pull_request: + paths: + - "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/.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 19a47ea..a7f825a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,246 @@ 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] - 2026-01-26 + +### 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 +- **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 + - **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 + - **Recipe Files** (`recipe.mdx`) - Complete deployment recipe documentation with all task actions + - **Logging System** (`logs.mdx`) - Persistent logging with file rotation and configuration + - **Custom Server Logs** (`custom-server-log.mdx`) - Guide for logging custom commands + - **Color Palettes** (`palettes.mdx`) - Theming and palette configuration + - **Translation Support** (`translation.mdx`) - Contributing translations and custom locale setup +- **Guidelines Modal Enhancement** - Improved markdown link parsing in partnership guidelines + - Added regex-based link parsing for `[text](url)` markdown syntax + - 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 +- **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 + +#### 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 +- **Navigation Enhancement** - Improved main navigation configuration + - Hosting menu item with Server icon (green), banner, and description + - Brand menu item with Palette icon (pink), banner, and description + - 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 + +- **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 + +- **Guidelines Modal Component Architecture** - Fixed server/client component boundary issues + - Removed context provider wrapper that caused hook usage errors in server components + - Implemented direct client-side state management in hosting page + - Fixed import paths for component libraries + - Removed unnecessary context provider files + +- **GitHub URL References** - Fixed erroneous `frontend/` directory in provider documentation links + - Updated GUIDELINES.md to use correct GitHub repository URLs + - Removed local folder structure references from remote URLs + - All links now point to correct paths in main FixFX repository + +- **txAdmin Documentation Link Format** - Fixed internal page links in all txAdmin docs + - Removed `.mdx` and `.md` extensions from internal page references + - Updated all GitHub URLs to official `citizenfx/txAdmin` repository + - 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` + - **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 #### 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`) @@ -20,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` @@ -27,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 @@ -37,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 @@ -54,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 @@ -96,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 @@ -103,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 @@ -164,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 e517fc1..0cea8f8 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,189 @@ -# 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. + +> [!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 -- 📚 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](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) + +### 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/cYauqJfnNK](https://discord.gg/cYauqJfnNK) +- **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. 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 1832a2d..1c31c68 100644 --- a/app/(blog)/blog/(root)/page.tsx +++ b/app/(blog)/blog/(root)/page.tsx @@ -1,65 +1,71 @@ 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', - year: 'numeric' +
+ + + {new Date(post.data.date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", })}
)} {/* Title */} -

+

{post.data.title}

{/* Description */} -

+

{post.data.description}

{/* Read more link */} -
+
Read article - +
@@ -68,8 +74,10 @@ 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..48e00da 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,71 @@ 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..d5ba57a --- /dev/null +++ b/app/(blog)/blog/atom.xml/route.ts @@ -0,0 +1,50 @@ +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..3d0b95c --- /dev/null +++ b/app/(blog)/blog/feed.json/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 = { + 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..34a22a2 --- /dev/null +++ b/app/(blog)/blog/feed.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 + ${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..623048b 100644 --- a/app/(blog)/opengraph-image.tsx +++ b/app/(blog)/opengraph-image.tsx @@ -11,43 +11,119 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
+ ✍️ +
+ + {/* Main title */} +
-
- FixFX Blog -
-
+ - Latest updates and insights from the FiveM community -
+ 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..29fc779 100644 --- a/app/(blog)/twitter-image.tsx +++ b/app/(blog)/twitter-image.tsx @@ -3,50 +3,96 @@ 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"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ ✍️ +
+ + {/* Main title */}
-
- FixFX -
-
+ - Blog -
+ FX +
- ), + + {/* Subtitle */} +

+ Blog +

+
, { ...size, }, diff --git a/app/(docs)/docs/[...slug]/page.tsx b/app/(docs)/docs/[...slug]/page.tsx index 3655e13..a587eca 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) { - return source.getPage(['overview']); + 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 ( @@ -42,7 +45,9 @@ export default async function Page({ + props.src ? : null, + Editor: Editor, }} /> diff --git a/app/(docs)/docs/layout.tsx b/app/(docs)/docs/layout.tsx index 598fa49..9e7feb1 100644 --- a/app/(docs)/docs/layout.tsx +++ b/app/(docs)/docs/layout.tsx @@ -1,9 +1,29 @@ 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"; 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..a1b2cab 100644 --- a/app/(docs)/opengraph-image.tsx +++ b/app/(docs)/opengraph-image.tsx @@ -11,43 +11,144 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
+ 📚 +
+ + {/* Main title */} +
-
- FixFX Docs -
-
+ - Comprehensive guides and information for the CitizenFX ecosystem -
+ FX + + + Docs + +
+ + {/* 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 38e3735..3f08902 100644 --- a/app/(docs)/twitter-image.tsx +++ b/app/(docs)/twitter-image.tsx @@ -3,50 +3,96 @@ 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"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ 📚 +
+ + {/* Main title */}
-
- FixFX -
-
+ - Docs -
+ 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 a8737b6..7868c8f 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, @@ -11,52 +11,178 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */} +
+
+ + Open Source Documentation + +
+ + {/* Main title */}
-
- FixFX -
-
+ - Comprehensive guides for the CitizenFX ecosystem + FX + +
+ + {/* Subtitle */} +

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

+ + {/* Bottom indicators */} +
+
+
+ + Free & Open Source +
-
- Powered by the Community +
+
+ + Community Driven + +
+
+
+ + Always Updated +
- ), +
, { ...size, }, diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index 64e400d..4bc12df 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -3,6 +3,23 @@ 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..cedef1d 100644 --- a/app/(landing)/twitter-image.tsx +++ b/app/(landing)/twitter-image.tsx @@ -1,52 +1,100 @@ 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"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Main title */}
-
- FixFX -
-
+ - FiveM Guides -
+ 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 new file mode 100644 index 0000000..ebba99f --- /dev/null +++ b/app/api/guidelines/route.ts @@ -0,0 +1,20 @@ +import { readFile } from "fs/promises"; +import { join } from "path"; + +export async function GET() { + try { + const filePath = join(process.cwd(), "packages/providers/GUIDELINES.md"); + const content = await readFile(filePath, "utf-8"); + + return Response.json({ + success: true, + content, + }); + } catch (error) { + console.error("Error reading guidelines:", error); + return Response.json( + { success: false, error: "Failed to read guidelines" }, + { status: 500 }, + ); + } +} diff --git a/app/api/providers/route.ts b/app/api/providers/route.ts new file mode 100644 index 0000000..a0af51a --- /dev/null +++ b/app/api/providers/route.ts @@ -0,0 +1,17 @@ +import { getHostingProviders } from "@/lib/providers"; + +export async function GET() { + try { + const providers = await getHostingProviders(); + return Response.json({ + success: true, + providers, + }); + } catch (error) { + console.error("Error fetching providers:", error); + return Response.json( + { success: false, error: "Failed to fetch providers" }, + { status: 500 }, + ); + } +} diff --git a/app/api/trusted-hosts/route.ts b/app/api/trusted-hosts/route.ts new file mode 100644 index 0000000..48044f5 --- /dev/null +++ b/app/api/trusted-hosts/route.ts @@ -0,0 +1,17 @@ +import { getTrustedHosts } from "@/lib/trusted-hosts"; + +export async function GET() { + try { + const hosts = await getTrustedHosts(); + return Response.json({ + success: true, + hosts, + }); + } catch (error) { + console.error("Error fetching trusted hosts:", error); + return Response.json( + { success: false, error: "Failed to fetch trusted hosts" }, + { status: 500 }, + ); + } +} diff --git a/app/apple-icon.tsx b/app/apple-icon.tsx new file mode 100644 index 0000000..6113ee7 --- /dev/null +++ b/app/apple-icon.tsx @@ -0,0 +1,93 @@ +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..1440b17 100644 --- a/app/artifacts/layout.tsx +++ b/app/artifacts/layout.tsx @@ -1,8 +1,27 @@ 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({ @@ -10,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 fe62f1b..f2f5f17 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, @@ -11,43 +11,168 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
+ 📦 +
+ + {/* Main title */} +
+ + Fix + + + FX + + + Artifacts + +
+ + {/* Subtitle */} +

+ Download FiveM and RedM server builds with version tracking +

+ + {/* Status badges */} +
+ ✓ Recommended +
+
- FixFX Artifacts + Latest
- Access FiveM and RedM server artifacts + 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 7176bc4..79d55cc 100644 --- a/app/artifacts/twitter-image.tsx +++ b/app/artifacts/twitter-image.tsx @@ -1,52 +1,98 @@ 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"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ 📦 +
+ + {/* Main title */}
-
- FixFX -
-
+ - Artifacts -
+ FX +
- ), + + {/* Subtitle */} +

+ Artifacts +

+
, { ...size, }, diff --git a/app/banner-wide/route.tsx b/app/banner-wide/route.tsx new file mode 100644 index 0000000..267081a --- /dev/null +++ b/app/banner-wide/route.tsx @@ -0,0 +1,120 @@ +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..a784978 --- /dev/null +++ b/app/banner/route.tsx @@ -0,0 +1,120 @@ +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..49ad3ec --- /dev/null +++ b/app/brand/page.tsx @@ -0,0 +1,156 @@ +"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..9fdbd41 100644 --- a/app/chat/layout.tsx +++ b/app/chat/layout.tsx @@ -1,15 +1,30 @@ 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({ - 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 67997ab..4e8c01f 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, @@ -11,43 +11,138 @@ export const contentType = "image/png"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orbs */} +
+
+ + {/* Icon */}
+ 🤖 +
+ + {/* Main title */} +
-
- FixFX Chat -
-
+ - AI-powered assistance for FiveM development -
+ FX + + + Chat + +
+ + {/* 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 7113770..9135607 100644 --- a/app/chat/twitter-image.tsx +++ b/app/chat/twitter-image.tsx @@ -1,52 +1,98 @@ 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"; export default function Image() { return new ImageResponse( - ( +
+ {/* Background gradient orb */} +
+ + {/* Icon */} +
+ 🤖 +
+ + {/* Main title */}
-
- FixFX -
-
+ - Chat -
+ FX +
- ), + + {/* Subtitle */} +

+ Chat +

+
, { ...size, }, diff --git a/app/components/image-modal.tsx b/app/components/image-modal.tsx deleted file mode 100644 index 41bd532..0000000 --- a/app/components/image-modal.tsx +++ /dev/null @@ -1,145 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { createPortal } from 'react-dom'; -import { X } from 'lucide-react'; - -interface ImageModalProps { - src: string; - alt: string; - title?: string; - width?: number; - height?: number; -} - -export function ImageModal({ src, alt, title, width, height }: ImageModalProps) { - const [isOpen, setIsOpen] = useState(false); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (isOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } - return () => { - document.body.style.overflow = 'unset'; - }; - }, [isOpen]); - - const modal = ( - <> - {/* Backdrop */} -
setIsOpen(false)} - /> - - {/* Modal Content */} -
- {/* Close Button */} - - - {/* Image Container */} -
- {alt} e.stopPropagation()} - /> - - {/* Title */} - {title && ( -

- {title} -

- )} -
-
- - ); - - return ( - <> - {/* Thumbnail */} - - - {/* Portal to body */} - {mounted && isOpen && createPortal(modal, document.body)} - - ); -} diff --git a/app/components/index.ts b/app/components/index.ts index 25bf6ef..cb75cb2 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -1,2 +1 @@ -export { FileSource } from './file-source'; -export { ImageModal } from './image-modal'; +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/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/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/favicon.ico b/app/favicon.ico index 92389de..fbfb4fa 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ 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 new file mode 100644 index 0000000..6b3eca5 --- /dev/null +++ b/app/hosting/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import { HomeLayout } from "fumadocs-ui/layouts/home"; +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.", + 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.", + url: "https://fixfx.wiki/hosting", + type: "website", + }, +}; + +export default function HostingLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/app/hosting/opengraph-image.tsx b/app/hosting/opengraph-image.tsx new file mode 100644 index 0000000..414f517 --- /dev/null +++ b/app/hosting/opengraph-image.tsx @@ -0,0 +1,178 @@ +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..81e2470 --- /dev/null +++ b/app/hosting/page.tsx @@ -0,0 +1,415 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { + Server, + Zap, + Shield, + Headphones, + ExternalLink, + Percent, + ChevronRight, + Handshake, + Cloud, +} from "lucide-react"; +import { ProviderCard, GuidelinesModal } from "@ui/components"; + +interface Provider { + id: string; + name: string; + website: string; + logo?: string; + description: string; + discount: { + percentage: number; + code: string; + }; + links: { + label: string; + url: string; + }[]; + features?: string[]; + isTrusted?: boolean; +} + +export default function HostingPage() { + const [guidelinesOpen, setGuidelinesOpen] = useState(false); + const [providers, setProviders] = useState([]); + const [providersWithTrust, setProvidersWithTrust] = useState([]); + const [loading, setLoading] = useState(true); + + // Fetch providers on mount + useEffect(() => { + const fetchProviders = async () => { + try { + const response = await fetch("/api/providers"); + const data = await response.json(); + 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(); + if (trustedData.success) { + const trustedHosts = trustedData.hosts; + const withTrust = providersData.map((provider) => ({ + ...provider, + isTrusted: provider.website + ? trustedHosts.some((host: any) => { + try { + const providerUrl = new URL( + provider.website, + ).hostname.toLowerCase(); + const hostUrl = new URL(host.url).hostname.toLowerCase(); + return ( + providerUrl === hostUrl || providerUrl.includes(hostUrl) + ); + } catch { + return false; + } + }) + : false, + })); + setProvidersWithTrust(withTrust); + } + } + } catch (error) { + console.error("Failed to fetch providers:", error); + } finally { + setLoading(false); + } + }; + + fetchProviders(); + }, []); + + 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"; + 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 */} +
+
+ {loading ? ( +
+ ) : ( +

+ {maxDiscount}%+ +

+ )} +

+ Exclusive Discounts +

+
+
+
+

24/7

+

+ Support Available +

+
+
+
+ {loading ? ( +
+ ) : ( +

+ {providers.length} +

+ )} +

+ Trusted Partners +

+
+
+
+
+
+ + {/* Partners Grid */} +
+ {loading ? ( +
+ {[1, 2].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : providersWithTrust.length > 0 ? ( +
+ {providersWithTrust.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 */} +
+ + + 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/twitter-image.tsx b/app/hosting/twitter-image.tsx new file mode 100644 index 0000000..414f517 --- /dev/null +++ b/app/hosting/twitter-image.tsx @@ -0,0 +1,178 @@ +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..278ce94 --- /dev/null +++ b/app/icon.tsx @@ -0,0 +1,92 @@ +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..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 } from "lucide-react"; +import { + Gamepad, + Home, + PlugZap, + LogsIcon, + Bot, + X, + Server, + Palette, +} from "lucide-react"; export const baseOptions: HomeLayoutProps = { disableThemeSwitch: true, @@ -18,15 +27,21 @@ export const baseOptions: HomeLayoutProps = { links: [ { type: "icon", - text: "", + text: "Discord", icon: , - url: "https://discord.gg/Vv2bdC44Ge", + url: "https://discord.gg/cYauqJfnNK", + }, + { + type: "icon", + text: "Twitter/X", + icon: , + url: "https://twitter.com/FixFXWiki", }, { type: "main", text: "Home", icon: , - url: "/" + url: "/", }, { type: "menu", @@ -69,8 +84,7 @@ export const baseOptions: HomeLayoutProps = { }, icon: , text: "Core Documentation", - description: - "Some information about FixFX.", + description: "Some information about FixFX.", url: "/docs/core", }, { @@ -103,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", }, { @@ -154,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", }, ], @@ -169,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: { @@ -189,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: { @@ -205,14 +217,48 @@ export const baseOptions: HomeLayoutProps = { Artifacts
- ) + ), }, icon: , text: "Server Artifacts", description: "Explore the latest server artifacts for CFX.", - url: "/artifacts" - } - ] - } + url: "/artifacts", + }, + { + menu: { + banner: ( +
+ +

+ Hosting Partners +

+
+ ), + }, + icon: , + text: "Hosting Partners", + description: + "Discover trusted hosting providers with exclusive discounts for FixFX users.", + url: "/hosting", + }, + { + menu: { + banner: ( +
+ +

+ Brand Assets +

+
+ ), + }, + icon: , + text: "Brand Assets", + description: + "Download official FixFX logos, icons, and brand guidelines.", + url: "/brand", + }, + ], + }, ], }; diff --git a/app/layout.tsx b/app/layout.tsx index 938bb83..8e81c35 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,33 @@ 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 +70,57 @@ 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,19 +128,151 @@ 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/cYauqJfnNK", + ], +}; + +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/cYauqJfnNK", + ], + 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 */} + - + ``` **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; } ``` @@ -109,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" }) @@ -125,10 +161,10 @@ end function CloseUI() if not isUIOpen then return end - + isUIOpen = false SetNuiFocus(false, false) - + SendNUIMessage({ action = "hide" }) @@ -149,7 +185,7 @@ end) Citizen.CreateThread(function() while true do Citizen.Wait(0) - + if isUIOpen and IsControlJustPressed(0, 322) then -- ESC key CloseUI() end @@ -160,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), + }); } ``` @@ -234,6 +271,7 @@ function sendToGame(action, data = {}) { ### Data Binding and Updates **client.lua** + ```lua local playerData = { name = '', @@ -246,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", @@ -262,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 @@ -342,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) ``` @@ -353,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", @@ -385,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); } ``` @@ -423,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 - - + + - - - + + +
- + ``` @@ -603,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); + } } ``` @@ -715,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); }); ``` @@ -742,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; + } } ``` @@ -767,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); } ``` @@ -797,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 3d7c99f..56ef447 100644 --- a/content/docs/cfx/resource-development/server-side.mdx +++ b/content/docs/cfx/resource-development/server-side.mdx @@ -3,8 +3,57 @@ 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"; + Server-side scripts handle game logic, player management, database operations, and security validation. They run on the server and communicate with clients through events. + + ## Server Script Basics ### Script Structure @@ -44,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) @@ -65,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 @@ -119,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) @@ -139,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, { @@ -148,7 +197,7 @@ RegisterCommand('heal', function(source, args, rawCommand) }) return end - + -- Execute command TriggerClientEvent('myresource:heal', source) end, false) @@ -211,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, @@ -263,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)) @@ -281,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 @@ -320,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) @@ -357,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 @@ -376,7 +425,7 @@ RegisterCommand('money', function(source, args, rawCommand) }) return end - + local amount = tonumber(args[1]) if amount then GivePlayerMoney(source, amount) @@ -391,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) @@ -410,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 @@ -423,7 +472,7 @@ function CheckSpamming(src, action, timeLimit, maxActions) actionCount = actionCount + 1 end end - + return actionCount >= maxActions end @@ -431,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) @@ -491,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 @@ -573,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 ``` @@ -611,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 @@ -645,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, @@ -673,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 13235d9..d27c7e9 100644 --- a/content/docs/cfx/support.mdx +++ b/content/docs/cfx/support.mdx @@ -4,6 +4,24 @@ 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"; + ## Official Links - [CitizenFX Collective](https://forum.cfx.re): Official forums for FiveM and RedM. diff --git a/content/docs/core/api/artifacts.mdx b/content/docs/core/api/artifacts.mdx index b2093a4..2c01ecb 100644 --- a/content/docs/core/api/artifacts.mdx +++ b/content/docs/core/api/artifacts.mdx @@ -4,10 +4,29 @@ description: Usage guides and information for our Artifacts API. icon: "PlugZap" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +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 @@ -21,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) @@ -45,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 @@ -122,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 @@ -155,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 @@ -177,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 dcdd1a0..6d57c73 100644 --- a/content/docs/core/api/chat.mdx +++ b/content/docs/core/api/chat.mdx @@ -4,10 +4,29 @@ description: AI-powered chat assistance for FiveM and RedM development. icon: "MessageCircle" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +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 @@ -21,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 @@ -67,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 @@ -110,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; } @@ -165,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 = { @@ -188,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:") @@ -209,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:") @@ -217,7 +237,7 @@ local function askFixie(question, framework, callback) print(i .. ". " .. suggestion) end end - + if callback then callback(data) end @@ -230,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; @@ -243,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 @@ -270,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); } @@ -286,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" ); @@ -315,7 +335,7 @@ if (response.Suggestions?.Any() == true) Console.WriteLine($"{index + 1}. {suggestion}"); } } -``` +```` @@ -330,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) { @@ -389,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?", @@ -399,8 +419,8 @@ streamChat( }, (finalResponse) => { // Handle completion - console.log('Chat completed:', finalResponse); - } + console.log("Chat completed:", finalResponse); + }, ); ``` @@ -422,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 = []; } @@ -495,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 @@ -514,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", ); ``` @@ -546,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+:/); @@ -567,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; @@ -583,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: @@ -610,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), }; } } @@ -629,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 @@ -649,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()); ``` @@ -668,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: @@ -693,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: @@ -709,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: @@ -725,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: @@ -741,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: @@ -759,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); } } @@ -781,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(); @@ -806,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 @@ -842,6 +858,7 @@ Standard HTTP status codes: - `500`: Server Error - AI service error Example error response: + ```json { "error": "Rate limit exceeded", @@ -883,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 ce5888e..2358176 100644 --- a/content/docs/core/api/contributors.mdx +++ b/content/docs/core/api/contributors.mdx @@ -4,10 +4,29 @@ description: Access GitHub contributors and repository statistics. icon: "Users" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +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 @@ -40,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 @@ -68,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 | @@ -124,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(); @@ -137,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`, + ); }); ``` @@ -162,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 @@ -207,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}"; @@ -220,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); } @@ -242,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}"); @@ -273,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} @@ -304,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}
@@ -322,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); }); ``` @@ -339,90 +363,90 @@ buildContributorsWidget().then(widget => { ```html - + Contributors Dashboard - - + +
- + - + ``` @@ -437,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; } @@ -493,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 @@ -561,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; } @@ -647,6 +686,7 @@ Standard HTTP status codes: - `503`: Service Unavailable - GitHub API issues Example error response: + ```json { "error": "Repository not found", @@ -687,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 a893bb9..34f1ec0 100644 --- a/content/docs/core/api/natives.mdx +++ b/content/docs/core/api/natives.mdx @@ -4,10 +4,29 @@ description: Access FiveM and RedM native functions and documentation. icon: "Code" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +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 @@ -39,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 @@ -99,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 @@ -114,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 } ], @@ -138,7 +157,7 @@ Get a specific native by its hash value. "data": [ { "name": "GET_ENTITY_HEALTH", - "hash": "0xebd235cf", + "hash": "0xebd235cf" // ... full native object } ], @@ -178,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'`); ``` @@ -186,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}`); }); ``` @@ -196,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`); ``` @@ -221,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 3f116c6..d1a1332 100644 --- a/content/docs/core/api/search.mdx +++ b/content/docs/core/api/search.mdx @@ -4,10 +4,29 @@ description: Search documentation and content across the FixFX platform. icon: "Search" --- -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +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 @@ -21,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 @@ -45,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 @@ -119,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(); @@ -131,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}`); }); } @@ -167,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:") @@ -194,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 @@ -228,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); } @@ -267,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) { @@ -277,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}"); @@ -313,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 @@ -398,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}

@@ -421,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); ``` @@ -443,152 +474,152 @@ const smartSearch = new SmartSearch(searchContainer); ```html - + Smart Search Interface - - + +
- + - + ``` @@ -605,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; } @@ -637,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); } @@ -655,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); }); ``` @@ -679,7 +711,7 @@ class SearchAnalytics { this.popularQueries = new Map(); this.categoryPreferences = new Map(); } - + trackSearch(query, results, category = null) { const searchEvent = { query, @@ -687,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, }; } } @@ -790,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(); @@ -815,6 +851,7 @@ analytics.generateReport(); ### Index Coverage The search index includes: + - All documentation pages and sections - Code examples and snippets - API endpoint descriptions @@ -825,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) @@ -856,6 +894,7 @@ Standard HTTP status codes: - `500`: Server Error - Search service error Example error response: + ```json { "error": "Invalid query", @@ -896,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 1ed1bc9..539f35c 100644 --- a/content/docs/core/disclaimer.mdx +++ b/content/docs/core/disclaimer.mdx @@ -4,8 +4,32 @@ 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"; + 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. @@ -18,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 2dac72b..c27fed3 100644 --- a/content/docs/core/faq.mdx +++ b/content/docs/core/faq.mdx @@ -4,30 +4,61 @@ description: Answers to common questions about FixFX. icon: "Info" --- -## What is FixFX? +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; -FixFX is a centralized documentation hub for the CitizenFX ecosystem, including FiveM and RedM. It provides guides, troubleshooting tips, and best practices for server management, framework integration, and resource development. - -## Is FixFX free to use? - -Yes, FixFX is completely free to use. Our goal is to provide accessible and comprehensive documentation for the CitizenFX community. - -## Who maintains FixFX? - -FixFX is maintained by a dedicated team of developers and contributors passionate about the CitizenFX ecosystem. You can contribute to FixFX by visiting our [GitHub repository](https://github.com/ByteBrushStudios/FixFX). - -## Does FixFX provide official support for CitizenFX? - -No, FixFX is not an official support platform for CitizenFX. It is a community-driven resource designed to help users navigate the CitizenFX ecosystem. - -## How can I contribute to FixFX? - -You can contribute by submitting pull requests, reporting issues, or suggesting new content on our [GitHub repository](https://github.com/ByteBrushStudios/FixFX). - -## Where can I report issues or suggest improvements? - -You can report issues or suggest improvements on our [GitHub Issues page](https://github.com/ByteBrushStudios/FixFX/issues). + - 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 09eb627..3ed2d0d 100644 --- a/content/docs/core/glossary.mdx +++ b/content/docs/core/glossary.mdx @@ -4,33 +4,81 @@ description: Definitions of common terms and acronyms in the CitizenFX ecosystem icon: "Book" --- -This glossary defines common terms and acronyms used in the CitizenFX ecosystem. +import { + FeatureList, + CheckList, + QuickLinks, + DefinitionList, + CommandCard, + CommandTable, + PropertyCard, + IconGrid, + Shortcut, + StatusBadge, + TroubleshootingCard, + PermissionTable, + ConfigBlock, + StepList, + ComparisonTable, +} from "@ui/components/mdx-components"; -## A - -### Artifact -A versioned build of the FXServer software used to host CitizenFX servers. - -## C - -### CitizenFX -The platform that powers FiveM and RedM, enabling multiplayer modifications for Grand Theft Auto V and Red Dead Redemption 2. - -## E - -### ESX -A popular framework for FiveM servers that provides roleplay features and economy systems. - -## F - -### FXServer -The server software used to host CitizenFX servers for FiveM and RedM. - -## Q - -### QBCore -A lightweight and modular framework for FiveM servers, designed for roleplay and economy-based gameplay. + - 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 5dccdf8..401bf32 100644 --- a/content/docs/core/index.mdx +++ b/content/docs/core/index.mdx @@ -5,30 +5,78 @@ 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"; 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. - - FixFX simplifies your journey through the CitizenFX platform. From server setup to troubleshooting and resource development, we have you covered. - + # 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. - - FixFX is continuously updated to ensure you have access to the latest information and solutions for the CitizenFX platform. - + # Why Choose FixFX? -FixFX empowers the CitizenFX community by providing: - -- **Comprehensive Documentation**: Covering everything from server setup to advanced scripting. -- **Troubleshooting Guides**: Solutions for common errors, crashes, and client/server issues. -- **Framework Integration**: Guides for ESX, QBCore, vRP, and other popular frameworks. -- **Server Management**: Best practices for hosting and optimizing your CitizenFX server. -- **Community Resources**: Curated tools, scripts, and resources to enhance your projects. + # Additional Information 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 a238f3c..2b43a38 100644 --- a/content/docs/frameworks/esx/development.mdx +++ b/content/docs/frameworks/esx/development.mdx @@ -4,914 +4,9 @@ description: Complete guide to developing resources for ESX framework. icon: "Code" --- -This guide covers everything you need to know about developing custom resources for ESX framework. +import { InfoBanner } from "@ui/components/mdx-components"; -## Development Environment Setup - -### Prerequisites - -- **Code Editor**: VS Code with Lua extensions recommended -- **Git**: For version control -- **Database Tool**: HeidiSQL, phpMyAdmin, or similar -- **ESX 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 "ESX 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 es_enableCustomData 1 -set esx_multicharacter_enabled true -``` - -## Resource Structure - -### Standard ESX Resource Structure - -``` -esx_resourcename/ -├── client/ -│ ├── main.lua -│ ├── events.lua -│ └── utils.lua -├── server/ -│ ├── main.lua -│ ├── events.lua -│ └── callbacks.lua -├── shared/ -│ ├── config.lua -│ └── locale.lua -├── html/ # For NUI resources -│ ├── index.html -│ ├── style.css -│ └── script.js -├── installation/ # Database files -│ └── esx_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 'ESX Resource Description' -version '1.0.0' -repository 'https://github.com/yourusername/esx_resourcename' - -shared_scripts { - '@es_extended/imports.lua', - '@es_extended/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' -} - -dependencies { - 'es_extended', - 'oxmysql' -} -``` - -## Core Integration - -### Getting ESX Object - -```lua --- Modern method (ESX Legacy) -ESX = exports['es_extended']:getSharedObject() - --- Legacy method (still supported) -ESX = nil -TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) - --- Alternative using imports --- @es_extended/imports.lua provides global ESX object -``` - -### Player Data Management - -#### Getting Player Data - -```lua --- Server-side -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer then - local playerData = xPlayer - local identifier = xPlayer.identifier - local job = xPlayer.job - local money = xPlayer.getMoney() -end - --- Client-side -local playerData = ESX.GetPlayerData() -if playerData then - local job = playerData.job - local accounts = playerData.accounts -end -``` - -#### Player Events - -```lua --- Client-side: Listen for player data updates -RegisterNetEvent('esx:playerLoaded', function(xPlayer) - ESX.PlayerData = xPlayer - -- Initialize your resource after player loads -end) - -RegisterNetEvent('esx:setJob', function(job) - ESX.PlayerData.job = job - -- Handle job updates -end) - -RegisterNetEvent('esx:setAccountMoney', function(account) - -- Handle money updates - for i=1, #ESX.PlayerData.accounts do - if ESX.PlayerData.accounts[i].name == account.name then - ESX.PlayerData.accounts[i] = account - break - end - end -end) - --- Server-side: Player management -AddEventHandler('esx:playerLoaded', function(playerId, xPlayer) - -- Handle player loading -end) - -AddEventHandler('esx:playerDropped', function(playerId, reason) - -- Handle player disconnect -end) -``` - -## Database Integration - -### Using oxmysql/mysql-async - -```lua --- SELECT query -MySQL.Async.fetchAll('SELECT * FROM users WHERE job = @job', { - ['@job'] = jobName -}, function(result) - if result[1] then - -- Handle results - end -end) - --- SELECT single row -MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = xPlayer.identifier -}, function(result) - if result then - -- Handle single result - end -end) - --- INSERT query -MySQL.Async.execute('INSERT INTO my_table (identifier, data) VALUES (@identifier, @data)', { - ['@identifier'] = xPlayer.identifier, - ['@data'] = json.encode(data) -}, function(affectedRows) - if affectedRows > 0 then - -- Success - end -end) - --- UPDATE query -MySQL.Async.execute('UPDATE users SET accounts = @accounts WHERE identifier = @identifier', { - ['@accounts'] = json.encode(accounts), - ['@identifier'] = xPlayer.identifier -}, function(affectedRows) - -- Handle update -end) -``` - -### Modern oxmysql (Promise-based) - -```lua --- Using promises (recommended) -local result = MySQL.query.await('SELECT * FROM users WHERE job = ?', {jobName}) -if result[1] then - -- Handle results -end - --- With error handling -local success, result = pcall(MySQL.query.await, 'SELECT * FROM users WHERE identifier = ?', {xPlayer.identifier}) -if success and result[1] then - -- Handle success -else - print('Database query failed') -end -``` - -## Job System - -### Creating Custom Jobs - -Add jobs to the database: - -```sql --- Insert job -INSERT INTO jobs (name, label) VALUES ('mechanic', 'Mechanic'); - --- Insert 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, '{}', '{}'), -('mechanic', 2, 'experienced', 'Experienced Mechanic', 600, '{}', '{}'), -('mechanic', 3, 'chief', 'Chief Mechanic', 800, '{}', '{}'); -``` - -### Job Management Functions - -```lua --- Server-side: Job management -local xPlayer = ESX.GetPlayerFromId(source) - --- Set player job -xPlayer.setJob('mechanic', 2) - --- Check job permissions -if xPlayer.job.name == 'police' and xPlayer.job.grade >= 3 then - -- Allow police captain+ actions -end - --- Get all players with specific job -local mechanics = ESX.GetExtendedPlayers('job', 'mechanic') - --- Client-side: Job checking -local playerData = ESX.GetPlayerData() -if playerData.job.name == 'mechanic' then - -- Mechanic-specific functionality -end -``` - -### Society System Integration - -```lua --- Server-side: Society functions -TriggerEvent('esx_society:getOnlinePlayers', function(players) - -- Get online players -end) - --- Get society account -TriggerEvent('esx_addonaccount:getSharedAccount', 'society_mechanic', function(account) - if account then - local balance = account.money - -- Use society money - end -end) - --- Add money to society -TriggerEvent('esx_addonaccount:getSharedAccount', 'society_mechanic', function(account) - if account then - account.addMoney(amount) - end -end) -``` - -## Money & Account System - -### Account Management - -```lua --- Server-side: Money functions -local xPlayer = ESX.GetPlayerFromId(source) - --- Get money -local cash = xPlayer.getMoney() -local bankMoney = xPlayer.getAccount('bank').money -local blackMoney = xPlayer.getAccount('black_money').money - --- Add money -xPlayer.addMoney(amount) -xPlayer.addAccountMoney('bank', amount) - --- Remove money -if xPlayer.getMoney() >= amount then - xPlayer.removeMoney(amount) -end - --- Set money -xPlayer.setMoney(amount) -xPlayer.setAccountMoney('bank', amount) - --- Client-side: Account checking -local playerData = ESX.GetPlayerData() -for i=1, #playerData.accounts do - if playerData.accounts[i].name == 'bank' then - local bankMoney = playerData.accounts[i].money - break - end -end -``` - -## Inventory System - -### Item Management - -```lua --- Server-side inventory functions -local xPlayer = ESX.GetPlayerFromId(source) - --- Add item -xPlayer.addInventoryItem('bread', 1) - --- Remove item -xPlayer.removeInventoryItem('water', 1) - --- Get item count -local itemCount = xPlayer.getInventoryItem('phone').count - --- Check if player has item -if xPlayer.getInventoryItem('lockpick').count > 0 then - -- Player has lockpick -end - --- Get weight -local currentWeight = xPlayer.getWeight() -local maxWeight = xPlayer.maxWeight - --- Check if can carry item -if xPlayer.canCarryItem('bread', 5) then - xPlayer.addInventoryItem('bread', 5) -end -``` - -### Useable Items - -```lua --- Server-side: Register useable item -ESX.RegisterUsableItem('bread', function(source) - local xPlayer = ESX.GetPlayerFromId(source) - - if xPlayer.getInventoryItem('bread').count > 0 then - xPlayer.removeInventoryItem('bread', 1) - TriggerClientEvent('esx_basicneeds:onEat', source, 'bread') - xPlayer.showNotification('You ate bread') - end -end) - --- Client-side: Handle item usage -RegisterNetEvent('esx_basicneeds:onEat', function(item) - -- Client-side effects for eating - local ped = PlayerPedId() - -- Play eating animation, etc. -end) -``` - -## Vehicle System - -### Vehicle Management - -```lua --- Server-side: Vehicle functions -local xPlayer = ESX.GetPlayerFromId(source) - --- Get player vehicles -MySQL.Async.fetchAll('SELECT * FROM owned_vehicles WHERE owner = @owner', { - ['@owner'] = xPlayer.identifier -}, function(vehicles) - -- Handle vehicles -end) - --- Add vehicle to player -local vehicleData = { - owner = xPlayer.identifier, - plate = 'ABC123', - vehicle = json.encode({model = GetHashKey('adder'), plate = 'ABC123'}), - type = 'car', - job = nil, - stored = 1 -} - -MySQL.Async.execute('INSERT INTO owned_vehicles (owner, plate, vehicle, type, job, stored) VALUES (@owner, @plate, @vehicle, @type, @job, @stored)', vehicleData) -``` - -### Vehicle Keys Integration - -```lua --- Give keys to player (if using esx_vehiclelock or similar) -TriggerEvent('esx_vehiclelock:giveKeys', source, plate) - --- Remove keys from player -TriggerEvent('esx_vehiclelock:removeKeys', source, plate) -``` - -## UI Development (NUI) - -### Basic NUI Setup - -```html - - - - - - - ESX 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://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! + diff --git a/content/docs/frameworks/esx/index.mdx b/content/docs/frameworks/esx/index.mdx index 2956b28..52642d5 100644 --- a/content/docs/frameworks/esx/index.mdx +++ b/content/docs/frameworks/esx/index.mdx @@ -4,186 +4,9 @@ description: The ESX framework for FiveM servers. icon: "Package" --- -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. +import { InfoBanner } from "@ui/components/mdx-components"; -## Overview - -ESX Legacy is a completely refactored framework offering enhanced performance, security, and modern FiveM compatibility: - -### ESX Legacy Features - -- **Modern Codebase** - Lua 5.4 support with optimized performance -- **ox_lib Integration** - Modern UI components and utilities -- **oxmysql Database** - Improved database performance and async queries -- **Player Management** - Enhanced multi-character support with identity system -- **Economy System** - Cash, bank, and black money with transaction logging -- **Job Framework** - Dynamic jobs with grade hierarchy and society banking -- **Inventory Management** - Weight-based system with item metadata support -- **Vehicle System** - Ownership, garages, keys, and modification persistence -- **Property Systems** - Real estate, apartments, and storage management -- **Society System** - Organization banking and employee management -- **Status Effects** - Hunger, thirst, and status effect system - -## Key Features - - - - Comprehensive character system with persistent data, multi-character support, and identity management. - - - - Dynamic job assignment with grades, salaries, and society banking for organizations. - - - - Multi-currency economy with cash, bank, and black money systems plus transaction logging. - - - - Weight-based inventory system with item categories and vehicle/property storage. - - - - Complete vehicle ownership system with garages, modifications, and impound functionality. - - - - Property ownership with customizable interiors and storage systems. - - - -## 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! + 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 9e4d5d8..42a3459 100644 --- a/content/docs/frameworks/esx/setup.mdx +++ b/content/docs/frameworks/esx/setup.mdx @@ -4,437 +4,9 @@ description: Complete guide to setting up and installing ESX framework. icon: "Download" --- -This guide covers the complete installation and initial setup of ESX framework for your FiveM server. +import { InfoBanner } from "@ui/components/mdx-components"; -## 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! + diff --git a/content/docs/frameworks/esx/troubleshooting.mdx b/content/docs/frameworks/esx/troubleshooting.mdx index 9cee7e2..f5c35a8 100644 --- a/content/docs/frameworks/esx/troubleshooting.mdx +++ b/content/docs/frameworks/esx/troubleshooting.mdx @@ -4,737 +4,9 @@ description: Common issues and solutions for ESX framework. icon: "Bug" --- -This guide covers common issues encountered when working with ESX framework and their solutions. +import { InfoBanner } from "@ui/components/mdx-components"; -## 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! + diff --git a/content/docs/frameworks/index.mdx b/content/docs/frameworks/index.mdx index cdf449c..7a3631d 100644 --- a/content/docs/frameworks/index.mdx +++ b/content/docs/frameworks/index.mdx @@ -4,6 +4,24 @@ 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"; + # Frameworks Overview FiveM supports various frameworks that provide structure, tools, and resources for server development. This section covers the most popular frameworks and how to use them effectively. @@ -12,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. + The most widely used framework for FiveM, providing a complete roleplay + foundation with modern updates. - + - A modern, modular framework with extensive features for roleplay servers. + A modern, modular framework with extensive features and flexibility for + roleplay servers. @@ -24,36 +44,68 @@ FiveM supports various frameworks that provide structure, tools, and resources f When selecting a framework for your FiveM server, consider: -1. **Community Support**: Larger communities typically mean more resources and faster support -2. **Documentation**: Well-documented frameworks are easier to learn and extend -3. **Performance**: Some frameworks are more optimized than others -4. **Features**: Different frameworks excel in different areas -5. **Development Activity**: Active development means bugs are fixed and features are added + ## Framework Components Most FiveM frameworks include: -- **Core Systems**: Player management, inventory, jobs, etc. -- **Database Integration**: For persistent data storage -- **UI Components**: For user interaction -- **Resource Management**: For handling server resources -- **API**: For extending functionality + -## Framework Integration +## Framework Integration Best Practices -Regardless of which framework you choose, follow these best practices: - -1. **Follow Documentation**: Use official guides when available -2. **Respect Dependencies**: Understand how resources depend on each other -3. **Maintain Compatibility**: Test thoroughly when upgrading -4. **Contribute**: Report bugs and contribute improvements -5. **Share Knowledge**: Document your solutions for others + - 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 2d95564..da61896 100644 --- a/content/docs/frameworks/qbcore/development.mdx +++ b/content/docs/frameworks/qbcore/development.mdx @@ -4,858 +4,9 @@ description: Complete guide to developing resources for QBCore framework. icon: "Code" --- -This guide covers everything you need to know about developing custom resources for QBCore framework. +import { InfoBanner } from "@ui/components/mdx-components"; -## 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! + diff --git a/content/docs/frameworks/qbcore/index.mdx b/content/docs/frameworks/qbcore/index.mdx index 94f9343..3fed1f9 100644 --- a/content/docs/frameworks/qbcore/index.mdx +++ b/content/docs/frameworks/qbcore/index.mdx @@ -4,91 +4,9 @@ description: A comprehensive guide to using and developing for the QBCore framew icon: "Component" --- -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. +import { InfoBanner } from "@ui/components/mdx-components"; -## 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 - -- **Player Management**: Multi-character support, data persistence, and permissions -- **Inventory System**: Advanced inventory with item metadata and attachments -- **Job System**: Comprehensive job framework with grades and permissions -- **Vehicle System**: Vehicle ownership, keys, and management -- **Property System**: Housing and property ownership -- **Business System**: Fully-featured business management -- **Phone System**: Integrated mobile phone with apps and messaging -- **Crafting System**: Item crafting and processing -- **Modern UI**: Clean and responsive user interfaces -- **Active Development**: Frequent updates and improvements - -## Documentation Sections - -### 🚀 [Setup & Installation](/docs/frameworks/qbcore/setup) -Complete guide to installing QBCore framework from scratch, including: -- Database setup and configuration -- Core framework installation -- Essential resources setup -- Server configuration -- Post-installation verification - -### 💻 [Development Guide](/docs/frameworks/qbcore/development) -Comprehensive development documentation covering: -- Resource development best practices -- QBCore API integration -- Database management -- Item and job system development -- UI/NUI development -- Testing and debugging - -### 🔧 [Troubleshooting](/docs/frameworks/qbcore/troubleshooting) -Common issues and their solutions: -- Installation problems -- Player data issues -- Performance optimization -- Resource-specific problems -- Debug techniques - -## Quick Start - -New to QBCore? Follow this recommended path: - -1. **[Setup & Installation](/docs/frameworks/qbcore/setup)** - Get QBCore running on your server -2. **[Development Guide](/docs/frameworks/qbcore/development)** - Learn to create custom resources -3. **[Troubleshooting](/docs/frameworks/qbcore/troubleshooting)** - Reference for when things go wrong - -## Community Resources - -- **[QBCore Documentation](https://docs.qbcore.org/)** - Official documentation -- **[QBCore GitHub](https://github.com/qbcore-framework)** - Source code and releases -- **[QBCore Discord](https://discord.gg/qbcore)** - Community support -- **[QBCore Forums](https://forum.cfx.re/t/qbcore-framework/4116649)** - Discussion and help - - - 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! + 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 786d649..4edb7a3 100644 --- a/content/docs/frameworks/qbcore/setup.mdx +++ b/content/docs/frameworks/qbcore/setup.mdx @@ -4,404 +4,9 @@ description: Complete guide to setting up and installing QBCore framework. icon: "Download" --- -This guide covers the complete installation and initial setup of QBCore framework for your FiveM server. +import { InfoBanner } from "@ui/components/mdx-components"; -## 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! + diff --git a/content/docs/frameworks/qbcore/troubleshooting.mdx b/content/docs/frameworks/qbcore/troubleshooting.mdx index 3824473..c0f1cd8 100644 --- a/content/docs/frameworks/qbcore/troubleshooting.mdx +++ b/content/docs/frameworks/qbcore/troubleshooting.mdx @@ -4,602 +4,9 @@ description: Common issues and solutions for QBCore framework. icon: "Bug" --- -This guide covers common issues encountered when working with QBCore framework and their solutions. +import { InfoBanner } from "@ui/components/mdx-components"; -## 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! + diff --git a/content/docs/guides/backup-recovery.mdx b/content/docs/guides/backup-recovery.mdx index c574e26..581137d 100644 --- a/content/docs/guides/backup-recovery.mdx +++ b/content/docs/guides/backup-recovery.mdx @@ -4,19 +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"; + 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 @@ -24,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 @@ -32,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 @@ -92,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" \ @@ -180,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" @@ -193,7 +237,7 @@ try { $BackupFile = "$BackupFile.zip" Write-Log "Backup compressed: $BackupFile" } - + # Verify backup if (Test-Path $BackupFile) { $BackupSize = (Get-Item $BackupFile).Length / 1MB @@ -204,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 { @@ -372,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" @@ -489,7 +533,7 @@ sudo systemctl list-timers | grep fivem ```yaml # docker-compose.backup.yml -version: '3.8' +version: "3.8" services: fivem-backup: @@ -702,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 @@ -730,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 @@ -763,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 @@ -813,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") @@ -829,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" @@ -852,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" \ @@ -865,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 new file mode 100644 index 0000000..1265e3b --- /dev/null +++ b/content/docs/guides/common-threats.mdx @@ -0,0 +1,569 @@ +--- +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"; + +## Introduction + +Your FiveM or RedM server is a valuable asset. It hosts data, manages player interactions, and runs custom code. This also makes it a target. Bad actors range from script kiddies running automated exploits to sophisticated attackers with specific knowledge of your infrastructure. Understanding the threat landscape is the first step to defending it. + +This guide covers the most common attack vectors, how they work, how to detect them, and how to prevent them. Consider it your server's security blueprint. + +## Types of Malicious Actors + +The people trying to compromise your server fall into several categories, each with different motivations and capabilities. + +**Script Kiddies** + +These are attackers with limited technical knowledge who use pre-built tools and scripts. They're the most common threat and typically run broad, indiscriminate attacks hoping something sticks. They lack the sophistication to target specific infrastructure but can still cause significant damage through sheer volume and persistence. + +**Disgruntled Users** + +Former admins, banned players, or unhappy staff members represent an insider threat. They have legitimate access credentials or knowledge of your infrastructure, which makes them particularly dangerous. They know your weak points because they've been inside your network. + +**Competitors** + +Other server owners may attempt to take down or compromise competing servers to drive players to their own. These attacks are often targeted and persistent. + +**Professional Threat Actors** + +Ransomware gangs and professional cybercriminals may target your server for financial gain. This tier operates at a higher level and may involve multiple attack vectors, reconnaissance, and patience. + +**Nation State Actors** + +Unlikely unless your server hosts something politically sensitive, but included for completeness. If you're worried about nation state actors, you have bigger problems than this guide can solve. + +## Attack Vector: Malicious Resources and Scripts + +The most common entry point for compromise is through custom resources and scripts. Your server runs code written by you, your developers, and potentially third-party developers. Any of these could be compromised or intentionally malicious. + +### Direct Code Exploitation + +A resource can perform almost any action on your server due to how FiveM's server-side scripting works. A malicious resource can: + +Read sensitive data from your database and exfiltrate it to remote servers. This includes player information, financial data, or administrative logs. The attacker simply connects to your database with the same credentials your server uses and downloads whatever they want. + +Modify server state to grant itself admin permissions, enable god mode, or create false evidence. Once administrative access is obtained, the attacker can do anything your admin system allows. + +Execute arbitrary commands on the host machine running your server. With insufficient sandboxing, a compromised resource can break out of the FiveM runtime and access the underlying operating system. + +Establish persistent access by creating backdoors that survive resource restarts or server reboots. The attacker doesn't need physical access to your machine to maintain control. + +Create hidden functionality that operates silently. A resource might appear normal to administrators while performing malicious activities in the background. The code that handles player reports might also be logging their conversations to a remote server. The resource that manages transactions might be skimming a percentage to the attacker's account. + +### Where Malicious Resources Come From + +Developers and server owners obtain resources from various sources, not all of them trustworthy. + +**Purchased or Downloaded Scripts** + +Resources acquired from marketplaces, GitHub, or purchased from developers may contain hidden functionality. Developers might include backdoors intentionally to maintain control of the resource and extract value from it. A resource sold to multiple servers can have a single backdoor benefiting the original developer. They monitor their backdoored resource across all servers, collecting player data or running resource-intensive tasks. + +Sometimes the resource is compromised after purchase. A developer's GitHub account is breached, and malicious code is added. The developer may not even realize their work has been compromised. Updates distributed to existing customers contain the malicious payload. + +**Custom Development** + +Even custom code written for your server can be compromised if the developer has malicious intent or their development environment has been compromised. A developer's laptop contains malware that injects code into resources before they're uploaded to your server. The developer has no idea this is happening because the injected code only activates under certain conditions they don't test for. + +**Open Source Resources** + +Open source doesn't mean secure. Many open source resources are abandoned or maintained by volunteers with limited security expertise. Vulnerabilities may exist and never be patched. Sometimes open source repositories are taken over by bad actors after the original maintainer loses interest. The repository's commit history is clean until suddenly it's not. + +### Identifying Malicious Resources + +Finding malicious code before it compromises your server requires vigilance and technical knowledge. + +