Rebuild website!

This commit is contained in:
Starystars67
2025-11-30 16:01:49 +00:00
parent 1671b775e4
commit 403489e43c
362 changed files with 77768 additions and 35940 deletions

7
src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup>
import DefaultLayout from './layouts/DefaultLayout.vue'
</script>
<template>
<DefaultLayout />
</template>

View File

@@ -1 +0,0 @@
2.0.81

View File

@@ -0,0 +1,121 @@
<script setup>
import { Github, Twitter, Facebook } from 'lucide-vue-next'
import { RouterLink } from 'vue-router'
</script>
<template>
<footer class="mt-auto py-8 border-t border-neutral-200 dark:border-neutral-800">
<div class="max-w-6xl mx-auto px-4">
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
<!-- Social Media Links -->
<div class="flex items-center gap-4">
<a
href="https://www.reddit.com/r/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-500 hover:text-beammp-blue transition-colors dark:text-neutral-400 dark:hover:text-blue-400"
aria-label="Reddit"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
/>
</svg>
</a>
<a
href="https://twitter.com/BeamMP_Mod_Team"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-500 hover:text-beammp-blue transition-colors dark:text-neutral-400 dark:hover:text-blue-400"
aria-label="Twitter"
>
<Twitter class="w-5 h-5" />
</a>
<a
href="https://www.facebook.com/BeamNGMP"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-500 hover:text-beammp-blue transition-colors dark:text-neutral-400 dark:hover:text-blue-400"
aria-label="Facebook"
>
<Facebook class="w-5 h-5" />
</a>
<a
href="https://github.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-500 hover:text-beammp-blue transition-colors dark:text-neutral-400 dark:hover:text-blue-400"
aria-label="GitHub"
>
<Github class="w-5 h-5" />
</a>
<a
href="https://discord.gg/beammp"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-500 hover:text-beammp-blue transition-colors dark:text-neutral-400 dark:hover:text-blue-400"
aria-label="Discord"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
/>
</svg>
</a>
<span class="hidden md:inline text-neutral-300 dark:text-neutral-600">|</span>
<a
href="https://www.patreon.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="text-xs text-orange-600 hover:text-orange-700 underline-offset-2 hover:underline dark:text-orange-400 dark:hover:text-orange-300"
>
Support on Patreon
</a>
</div>
<!-- Copyright and Legal -->
<div
class="flex flex-col items-center md:items-end gap-2 text-xs text-neutral-600 dark:text-neutral-500"
>
<p>&copy; 2019 - {{ new Date().getFullYear() }} | BeamMP Mod Team All Rights Reserved</p>
<div class="flex gap-3">
<RouterLink
to="/about"
class="text-neutral-700 hover:text-beammp-blue transition-colors dark:text-neutral-500 dark:hover:text-blue-400"
>
About
</RouterLink>
<span>&middot;</span>
<a
href="https://forum.beammp.com/topic/95/privacy-policy-v1-0"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-700 hover:text-beammp-blue transition-colors dark:text-neutral-500 dark:hover:text-blue-400"
>
Privacy Policy
</a>
<span>&middot;</span>
<a
href="https://forum.beammp.com/topic/94/terms-of-use-v1-0"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-700 hover:text-beammp-blue transition-colors dark:text-neutral-500 dark:hover:text-blue-400"
>
Terms &amp; Conditions
</a>
</div>
</div>
</div>
</div>
</footer>
</template>

View File

@@ -0,0 +1,261 @@
<script setup>
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu'
import { cn } from '@/lib/utils'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { Menu, X } from 'lucide-vue-next'
const mobileMenuOpen = ref(false)
function toggleMobileMenu() {
mobileMenuOpen.value = !mobileMenuOpen.value
}
function closeMobileMenu() {
mobileMenuOpen.value = false
}
</script>
<template>
<header
class="border-b border-neutral-200 dark:border-neutral-800 bg-white/80 dark:bg-neutral-900/70 backdrop-blur sticky top-0 z-20"
>
<nav class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
<RouterLink to="/" class="flex items-center gap-2 shrink-0" @click="closeMobileMenu">
<!-- Light mode logo -->
<img src="/beammp-logo.png" alt="BeamMP Logo" class="h-10 w-auto shrink-0 dark:hidden" />
<!-- Dark mode logo -->
<img
src="/beammp-logo-dark.png"
alt="BeamMP Logo"
class="h-10 w-auto shrink-0 hidden dark:block"
/>
</RouterLink>
<!-- Desktop Navigation -->
<!-- Switch to mobile earlier (avoid logo crowding) -->
<div class="hidden lg:flex items-center gap-4">
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<a
href="https://forum.beammp.com"
target="_blank"
rel="noopener noreferrer"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
Forum
</a>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<a
href="https://docs.beammp.com"
target="_blank"
rel="noopener noreferrer"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
Docs
</a>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<RouterLink
to="/communities"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
Communities
</RouterLink>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<RouterLink
to="/servers"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
Servers
</RouterLink>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<RouterLink
to="/stats"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
Statistics
</RouterLink>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<a
href="https://github.com/BeamMP/BeamMP"
target="_blank"
rel="noopener noreferrer"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
GitHub
</a>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink as-child>
<a
href="https://www.patreon.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
:class="
cn(
navigationMenuTriggerStyle(),
'bg-transparent text-neutral-900 hover:bg-neutral-100 hover:text-neutral-900 dark:bg-transparent dark:text-white dark:hover:bg-neutral-800 dark:hover:text-white'
)
"
>
Patreon
</a>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<ThemeToggle />
</div>
<!-- Mobile Menu Button and Theme Toggle -->
<div class="flex lg:hidden items-center gap-2">
<ThemeToggle />
<button
class="p-2 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
aria-label="Toggle menu"
@click="toggleMobileMenu"
>
<Menu v-if="!mobileMenuOpen" class="w-6 h-6" />
<X v-else class="w-6 h-6" />
</button>
</div>
</nav>
<!-- Mobile Menu -->
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div
v-if="mobileMenuOpen"
class="md:hidden border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900"
>
<div class="px-4 py-3 space-y-1">
<RouterLink
to="/about"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
About
</RouterLink>
<a
href="https://forum.beammp.com"
target="_blank"
rel="noopener noreferrer"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
Forum
</a>
<a
href="https://docs.beammp.com"
target="_blank"
rel="noopener noreferrer"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
Docs
</a>
<RouterLink
to="/communities"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
Communities
</RouterLink>
<RouterLink
to="/servers"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
Servers
</RouterLink>
<RouterLink
to="/stats"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
Statistics
</RouterLink>
<a
href="https://github.com/BeamMP/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
GitHub
</a>
<a
href="https://www.patreon.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="block px-4 py-3 text-neutral-900 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
@click="closeMobileMenu"
>
Patreon
</a>
</div>
</div>
</Transition>
</header>
</template>

View File

@@ -0,0 +1,78 @@
<script setup>
import { ref, onMounted } from 'vue'
import { Sun, Moon, Monitor } from 'lucide-vue-next'
const theme = ref('system')
const setTheme = (newTheme) => {
theme.value = newTheme
localStorage.setItem('theme', newTheme)
applyTheme(newTheme)
}
const applyTheme = (selectedTheme) => {
const root = document.documentElement
if (
selectedTheme === 'dark' ||
(selectedTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}
onMounted(() => {
const savedTheme = localStorage.getItem('theme') || 'system'
theme.value = savedTheme
applyTheme(savedTheme)
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (theme.value === 'system') {
applyTheme('system')
}
})
})
</script>
<template>
<div class="flex items-center gap-1 bg-neutral-200 dark:bg-neutral-800/50 rounded-lg p-1">
<button
:class="[
'p-2 rounded transition-colors',
theme === 'light'
? 'bg-white shadow-sm text-neutral-900 dark:bg-neutral-700 dark:text-white'
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-200',
]"
title="Light mode"
@click="setTheme('light')"
>
<Sun class="w-4 h-4" />
</button>
<button
:class="[
'p-2 rounded transition-colors',
theme === 'system'
? 'bg-white shadow-sm text-neutral-900 dark:bg-neutral-700 dark:text-white'
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-200',
]"
title="System theme"
@click="setTheme('system')"
>
<Monitor class="w-4 h-4" />
</button>
<button
:class="[
'p-2 rounded transition-colors',
theme === 'dark'
? 'bg-white shadow-sm text-neutral-900 dark:bg-neutral-700 dark:text-white'
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-200',
]"
title="Dark mode"
@click="setTheme('dark')"
>
<Moon class="w-4 h-4" />
</button>
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '.'
const props = defineProps({
variant: { type: null, required: false },
size: { type: null, required: false },
class: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false, default: 'button' },
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,33 @@
import { cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)

View File

@@ -0,0 +1,38 @@
<script setup>
import { reactiveOmit } from '@vueuse/core'
import { NavigationMenuRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import NavigationMenuViewport from './NavigationMenuViewport.vue'
const props = defineProps({
modelValue: { type: String, required: false },
defaultValue: { type: String, required: false },
dir: { type: String, required: false },
orientation: { type: String, required: false },
delayDuration: { type: Number, required: false },
skipDelayDuration: { type: Number, required: false },
disableClickTrigger: { type: Boolean, required: false },
disableHoverTrigger: { type: Boolean, required: false },
disablePointerLeaveClose: { type: Boolean, required: false },
unmountOnHide: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
})
const emits = defineEmits(['update:modelValue'])
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<NavigationMenuRoot
v-bind="forwarded"
:class="cn('relative z-10 flex max-w-max flex-1 items-center justify-center', props.class)"
>
<slot />
<NavigationMenuViewport />
</NavigationMenuRoot>
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { reactiveOmit } from '@vueuse/core'
import { NavigationMenuContent, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps({
forceMount: { type: Boolean, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
})
const emits = defineEmits([
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
])
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<NavigationMenuContent
v-bind="forwarded"
:class="
cn(
'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto',
props.class
)
"
>
<slot />
</NavigationMenuContent>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { reactiveOmit } from '@vueuse/core'
import { NavigationMenuIndicator, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps({
forceMount: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
})
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<NavigationMenuIndicator
v-bind="forwardedProps"
:class="
cn(
'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',
props.class
)
"
>
<div class="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuIndicator>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { NavigationMenuItem } from 'reka-ui'
const props = defineProps({
value: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
})
</script>
<template>
<NavigationMenuItem v-bind="props">
<slot />
</NavigationMenuItem>
</template>

View File

@@ -0,0 +1,18 @@
<script setup>
import { NavigationMenuLink, useForwardPropsEmits } from 'reka-ui'
const props = defineProps({
active: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
})
const emits = defineEmits(['select'])
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<NavigationMenuLink v-bind="forwarded">
<slot />
</NavigationMenuLink>
</template>

View File

@@ -0,0 +1,24 @@
<script setup>
import { reactiveOmit } from '@vueuse/core'
import { NavigationMenuList, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
})
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<NavigationMenuList
v-bind="forwardedProps"
:class="cn('group flex flex-1 list-none items-center justify-center gap-x-1', props.class)"
>
<slot />
</NavigationMenuList>
</template>

View File

@@ -0,0 +1,31 @@
<script setup>
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown } from 'lucide-vue-next'
import { NavigationMenuTrigger, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { navigationMenuTriggerStyle } from '.'
const props = defineProps({
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
})
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<NavigationMenuTrigger
v-bind="forwardedProps"
:class="cn(navigationMenuTriggerStyle(), 'group', props.class)"
>
<slot />
<ChevronDown
class="relative top-px ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuTrigger>
</template>

View File

@@ -0,0 +1,31 @@
<script setup>
import { reactiveOmit } from '@vueuse/core'
import { NavigationMenuViewport, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps({
forceMount: { type: Boolean, required: false },
align: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
})
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<div class="absolute left-0 top-full flex justify-center">
<NavigationMenuViewport
v-bind="forwardedProps"
:class="
cn(
'origin-top-center relative mt-1.5 h-[--reka-navigation-menu-viewport-height] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[--reka-navigation-menu-viewport-width] left-[var(--reka-navigation-menu-viewport-left)]',
props.class
)
"
/>
</div>
</template>

View File

@@ -0,0 +1,14 @@
import { cva } from 'class-variance-authority'
export { default as NavigationMenu } from './NavigationMenu.vue'
export { default as NavigationMenuContent } from './NavigationMenuContent.vue'
export { default as NavigationMenuIndicator } from './NavigationMenuIndicator.vue'
export { default as NavigationMenuItem } from './NavigationMenuItem.vue'
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
export { default as NavigationMenuList } from './NavigationMenuList.vue'
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'
export const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50'
)

View File

@@ -0,0 +1,19 @@
<script setup>
import { RouterView } from 'vue-router'
import AppNavigation from '@/components/AppNavigation.vue'
import AppFooter from '@/components/AppFooter.vue'
</script>
<template>
<div
class="min-h-screen flex flex-col bg-white text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100"
>
<AppNavigation />
<main class="flex-1 w-full">
<div class="max-w-6xl mx-auto px-4 py-10">
<RouterView />
</div>
</main>
<AppFooter />
</div>
</template>

6
src/lib/lib/utils.js Normal file
View File

@@ -0,0 +1,6 @@
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs) {
return twMerge(clsx(inputs))
}

6
src/lib/utils.js Normal file
View File

@@ -0,0 +1,6 @@
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs) {
return twMerge(clsx(inputs))
}

6
src/main.js Normal file
View File

@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './routes'
import './style.css'
createApp(App).use(router).mount('#app')

View File

@@ -1,104 +0,0 @@
var express = require('express');
var router = express.Router();
function mainRoutes(router) {
router.get('/', function (req, res) {
res.render('index.ejs');
});
router.get('/servers', function (req, res) {
res.render('servers.ejs');
});
router.get('/stats', function (req, res) {
res.render('stats.ejs');
});
router.get('/ping', function (req, res) {
res.send('OK');
});
/**
* Backwards Compat for OLD launcher versions
*/
router.get('/builds/launcher', function(req, res) {
if (req.query.download == 'true') {
//res.download('https://backend.beammp.com/builds/launcher?download=true')
const file = `${__dirname}/builds/launcher/launcher.exe`;
res.download(file); // Set disposition and send it.
} else if (req.query.version == 'true') {
//res.redirect('https://backend.beammp.com/builds/launcher?version=true')
const file = `${__dirname}/builds/launcher/version.json`;
res.download(file); // Set disposition and send it.
} else {
//res.sendStatus(403)
res.send("Not Found!")
}
});
/**
* Redirects
*/
router.get('/keymaster', function (req, res) {
res.redirect('https://keymaster.beammp.com/');
});
router.get('/k/keys', function (req, res) {
res.redirect('https://keymaster.beammp.com/');
});
router.get('/k/dashboard', function (req, res) {
res.redirect('https://keymaster.beammp.com/');
});
}
module.exports = function (app) {
try {
mainRoutes(router)
} catch (e) {
console.error(e)
}
app.use('/', router)
app.use(handle404)
app.use(handleErrors)
app.use(clientErrorHandler)
}
function handleErrors(err, req, res) {
const status = err.status || 500
res.status(err.status)
if (status === 429) {
res.render('429', { layout: false })
return
}
if (status === 500) {
res.render('500', { layout: false })
return
}
if (status === 503) {
res.render('503', { layout: false })
return
}
winston.warn(err.stack)
res.json({
message: err.message,
error: err,
})
}
function handle404(req, res) {
return res.status(404).send('404')
}
function clientErrorHandler(err, req, res, next) {
if (req.xhr) {
res.status(500).send({ error: 'Something failed!' })
} else {
next(err)
}
}

96
src/routes/index.js Normal file
View File

@@ -0,0 +1,96 @@
import { createRouter, createWebHistory } from 'vue-router'
import NotFound from '@/views/NotFound.vue'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: 'Home - BeamMP',
description: 'Welcome to BeamMP',
requiresAuth: false,
},
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: {
title: 'About - BeamMP',
description: 'Learn more about BeamMP',
requiresAuth: false,
},
},
/*{
path: '/contact',
name: 'Contact',
component: Contact,
meta: {
title: 'Contact - BeamMP',
description: 'Get in touch with us',
requiresAuth: false
}
},*/
{
path: '/communities',
name: 'Communities',
component: () => import('@/views/Communities.vue'),
meta: {
title: 'Communities - BeamMP',
description: 'Discover thriving BeamMP communities and find your group',
requiresAuth: false,
},
},
{
path: '/servers',
name: 'Servers',
component: () => import('@/views/Servers.vue'),
meta: {
title: 'Servers - BeamMP',
description: 'Browse active BeamMP servers',
requiresAuth: false,
},
},
{
path: '/stats',
name: 'Statistics',
component: () => import('@/views/Statistics.vue'),
meta: {
title: 'Statistics - BeamMP',
description: 'View BeamMP network statistics',
requiresAuth: false,
},
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound,
meta: {
title: 'Page Not Found - BeamMP',
description: 'The page you are looking for does not exist',
requiresAuth: false,
},
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// Global navigation guard for meta data
router.beforeEach((to, from, next) => {
// Set page title
document.title = to.meta.title || 'BeamMP'
// Set meta description
const metaDescription = document.querySelector('meta[name="description"]')
if (metaDescription) {
metaDescription.setAttribute('content', to.meta.description || '')
}
next()
})
export default router

File diff suppressed because it is too large Load Diff

View File

@@ -1,645 +0,0 @@
/* --------- NAVBAR // GLOBAL --------- */
* {
font-family: "Fira Sans", "Helvetica", Arial, sans-serif, serif;
box-sizing: border-box;
padding: 0;
margin: 0;
}
.container {
padding: 0 20px;
}
.mobile-container {
width: 250px;
max-width: 480px;
margin: auto;
background-color: rgb(236, 188, 132);
max-height: 500px;
color: white;
border-radius: 10px;
overflow: hidden;
z-index: 1000;
}
/*.topnav {*/
/* overflow: hidden;*/
/* position: relative;*/
/*}*/
/*.topnav #links {*/
/* display: none;*/
/*}*/
/*.topnav a {*/
/* color: white;*/
/* padding: 14px 16px;*/
/* text-decoration: none;*/
/* font-size: 17px;*/
/* display: hidden;*/
/*}*/
/*.topnav a.icon {*/
/* display: hidden;*/
/* position: absolute;*/
/* right: 0;*/
/* top: 0;*/
/*}*/
.nav {
position: fixed;
z-index: 100;
height: 80px;
font-weight: 400px;
overflow: visable;
background-color: rgb(236, 188, 132);
width: 100%;
line-height: 1;
transition: box-shadow .2s ease;
}
.nav .links{
display: flex;
height: 100%;
float: right;
}
.nav a {
color: #161923;
text-decoration: none;
transition: background-color .2s ease;
}
.nav .links li {
height: 100%;
padding-top: 20px;
list-style: none;
font-size: 16px;
}
/* li{
display: list-item;
text-align: -webkit-match-parent;
padding: 10px;
} */
nav li {
display: list-item;
text-align: -webkit-match-parent;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
}
.logo-link {
display: flex;
height: 100%;
margin-left: -15px;
padding: 20px 15px;
align-items: center;
align-content: center;
}
.topnav {
display: none;
overflow: hidden;
position: relative;
}
.topnav #burger-links {
display: hidden;
}
.topnav a {
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
display: hidden;
}
.topnav a.icon {
display: hidden;
position: fixed;
right: 0;
top: 0;
}
.logo-text {
font-size: 20px;
margin-left: 15px;
}
.logo-image {
width: 200px;
height: 200px;
/*border-radius: 50%;*/
/*margin-top: -57px;*/
}
.burger-menu {
display: none;
}
@media screen and (max-width: 990px) {
.navbar-links {
display: none;
}
.burger-menu {
display: block;
margin-top: 25px;
float: right;
}
/*.topnav {*/
/* display: block;*/
/* overflow: hidden;*/
/* position: relative;*/
/*}*/
/*.topnav #myLinks {*/
/* display: hidden;*/
/*}*/
/*.topnav a {*/
/* padding: 14px 16px;*/
/* text-decoration: none;*/
/* font-size: 17px;*/
/* display: block;*/
/*}*/
/*.topnav a.icon {*/
/* display: block;*/
/* position: absolute;*/
/* right: 0;*/
/* top: 0;*/
/*}*/
}
@media screen and (max-width: 575px) {
.burger-menu {
display: block;
margin-top: -25px;
float: right;
}
}
@media screen and (max-width: 360px) {
.burger-menu {
display: block;
margin-top: -43%;
float: right;
}
}
@media screen and (max-width: 411px) {
.burger-menu {
display: block;
margin-top: -38%;
float: right;
}
}
@media screen and (max-width: 320px) {
.burger-menu {
display: block;
margin-top: -50%;
float: right;
}
}
@media screen and (max-width: 375px) {
.burger-menu {
display: block;
margin-top: -42%;
float: right;
}
}
@media screen and (max-width: 414px) {
.burger-menu {
display: block;
margin-top: -37%;
float: right;
}
}
@media screen and (max-width: 375px) {
.burger-menu {
display: block;
margin-top: -41%;
float: right;
}
}
@media screen and (max-width: 767px) {
.text {
display: none;
}
.logo-link {
display: flex;
height: 100%;
align-items: center;
align-content: center;
}
.logo-text {
margin: 0 auto;
}
}
/* ----------- IMAGE AND DOWNLOAD ----------- */
.main-content {
display: flex;
position: relative;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
width: 100%;
height: calc(100vh - 80px);
overflow: hidden;
background: linear-gradient(-45deg, rgba(22, 25, 35, 0.35) 0%, rgba(22, 25, 35, 0.95) 100%) center center/100%, url(../img/beamng-mp-landing.png) center top/cover;
color: #fefee1;
}
.main-content .introduction {
width: 100%;
position: absolute;
transition: transform .2s ease;
transform: translateX(0);
}
p {
display: block;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
color: #c8cdbb;
}
.main-content .lead {
font-size: 30px;
font-weight: 300;
font-family: verdana;
}
.middle-xs {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.center-xs {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.main-content .buttons-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
align-content: flex-end;
}
.main-content .buttons-wrapper .buttons .download-client {
display: block;
position: relative;
padding: 15px 25px 15px 25px;
cursor: pointer;
color: #fefee1;
border: none;
text-shadow: 0 0 1px rgba(22, 25, 35, 0.5);
background: radial-gradient(circle, rgba(150, 204, 0, 0.8), rgba(150, 204, 0, 0.65)) center/100%;
transition: box-shadow .2s ease;
font-size: 27px;
font-weight: 300;
line-height: 1;
border-radius: 3px;
text-decoration: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
min-width: 297px;
}
.description {
display: block;
font-size: 12px;
font-weight: 200;
opacity: .9;
}
div.buttons {
min-width: 297px;
}
div.buttons.mt-3 {
min-width: 297px;
}
img.os-icon {
width: 32px !important;
height: 32px !important;
margin-left: 0 !important;
}
@media screen and (max-width: 600px) {
#button {
display: none;
}
}
/* --------- Features ---------*/
.features article.col-lg-3 {
padding: 0 35px;
}
.features article {
margin-top: 50px;
font-size: 15px;
color: #c8cdbb;
}
section {
padding-bottom: 50px;
}
/* @media only screen and {min-width: 75em}
.col-lg-3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%;
}
@media only screen and {min-width: 75em}
.col-lg, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-offset-0, .col-lg-offset-1, .col-lg-offset-2, .col-lg-offset-3, .col-lg-offset-4, .col-lg-offset-5, .col-lg-offset-6, .col-lg-offset-7, .col-lg-offset-8, .col-lg-offset-9, .col-lg-offset-10, .col-lg-offset-11, .col-lg-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
@media only screen and {min-width: 64em}
.col-md-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
@media only screen and {min-width: 64em}
.col-md, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md-offset-0, .col-md-offset-1, .col-md-offset-2, .col-md-offset-3, .col-md-offset-4, .col-md-offset-5, .col-md-offset-6, .col-md-offset-7, .col-md-offset-8, .col-md-offset-9, .col-md-offset-10, .col-md-offset-11, .col-md-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
@media only screen and {min-width: 48em}
.col-sm-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
@media only screen and {min-width: 48em}
.col-sm, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-offset-0, .col-sm-offset-1, .col-sm-offset-2, .col-sm-offset-3, .col-sm-offset-4, .col-sm-offset-5, .col-sm-offset-6, .col-sm-offset-7, .col-sm-offset-8, .col-sm-offset-9, .col-sm-offset-10, .col-sm-offset-11, .col-sm-offset-12 {
box-sizing: border-box;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
} */
/* #features {
font-size: 16px;
font-weight: 300;
color: #fefee1;
background-color: #161923;
} */
body {
font-size: 16px;
font-weight: 300;
color: #fefee1;
background-color: #161923 !important;
}
/* --------- Bridge ---------- */
.container-fluid, .container {
margin-right: auto;
margin-left: auto;
}
hr {
margin-top: 50px;
border-top: 1px solid rgba(250, 255, 225, 0.15);
}
figure {
max-width: 100%;
}
img {
margin-left: 40%;
width: 572px;
height: 332px;
}
figure img {
max-width: 100%;
border-radius: 3px;
filter: contrast(0.75);
transition: filter .2s ease;
width: 572px;
height: 332px;
}
figure {
display: block;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 40px;
margin-inline-end: 40px;
}
@media only screen and (min-width: 75em)
.middle-lg {
-webkit-box-align: center;
align-items: center;
}
@media screen and (max-width: 600px) { /* Phones */
figure img {
width: 336px;
height: 189px;
}
img {
margin-left: 6%;
}
}
@media screen and (max-width: 768px) { /* Ipad */
img {
margin-left: 10%;
}
}
@media screen and (max-width: 1024px) { /* Ipad Pro */
img {
margin-left: 6%;
}
}
@media screen and (max-width: 1444px) { /* Responsive help */
img {
margin-left: 6%;
}
}
@media only screen and (min-width: 75em)
.col-lg-6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%;
}
.user-features-list {
display: inline-block;
margin-top: 50px;
}
@media screen and (max-width: 500px) {
.user-features-list {
display: inline-block;
margin-top: 50px;
padding-left: 50px;
}
}
.user-features-list li {
text-align: left;
font-size: 30px;
font-weight: 200;
}
section ul li {
margin-bottom: 9.80392px;
color: #c8cdbb;
}
/* --------- FAQ ---------- */
#faq h1 {
text-align: center;
margin-top: 50px;
font-size: 40px;
font-weight: 100;
line-height: 1;
color: #fefee1;
}
.faq h3 {
position: relative;
margin: 25px 0 0 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
color: #fefee1;
}
h3 {
font-weight: 400;
font-size: 20px;
line-height: 1.2;
}
/* -------- FOOTER -------- */
footer {
min-height: 150px;
background-color: #403F4C;
padding-left: 25px;
}
.text-white {
color: #fff !important;
}
.row {
max-width: 100%;
}
/* .row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
} */
.text-right {
text-align: right !important;
}
.list-inline {
padding-left: 0;
list-style: none;
}
.py-5 {
padding-bottom: 3rem !important;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
footer .container {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
footer .row {
width: 100%;
justify-content: space-around;
}
.row i {
font-size: 22px;
margin: 0 3px;
}
@media screen and (max-width: 600px) {
.container {
width: 100%;
}
footer .container {
width: 80%;
margin: 0 auto;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,362 +0,0 @@
$(function () {
$.getJSON("https://backend.beammp.com/servers-info", function (data) {
$('#Servers-List').empty();
//console.log("Data Received:")
//console.log(data)
$.each(data, function (k, v) {
//var v = data[k][Object.keys(data[k])[0]]
//console.log(v)
$('#Servers-List').append(`
<tr>
<td>${getCountryName(v.location)}</td>
<td>${formatServerName(v.sname)}</td>
<td>${SmoothMapName(v.map)}</td>
<td>${v.players}/${v.maxplayers}</td>
</tr>`
);
});
$('#dataTable').DataTable();
});
});
var styleMap = {
'^0': 'color:#000000',
'^1': 'color:#0000AA',
'^2': 'color:#00AA00',
'^3': 'color:#00AAAA',
'^4': 'color:#AA0000',
'^5': 'color:#AA00AA',
'^6': 'color:#FFAA00',
'^7': 'color:#AAAAAA',
'^8': 'color:#555555',
'^9': 'color:#5555FF',
'^a': 'color:#55FF55',
'^b': 'color:#55FFFF',
'^c': 'color:#FF5555',
'^d': 'color:#FF55FF',
'^e': 'color:#FFFF55',
'^f': 'color:#FFFFFF',
'^l': 'font-weight:bold',
'^m': 'text-decoration:line-through',
'^n': 'text-decoration:underline',
'^o': 'font-style:italic',
};
function applyCode(string, codes) {
var elem = document.createElement('span');
string = string.replace(/\x00*/g, '');
for (var i = 0, len = codes.length; i < len; i++) {
elem.style.cssText += styleMap[codes[i]] + ';';
}
elem.innerHTML = string;
return elem;
}
function formatServerName(string) {
var codes = string.match(/\^.{1}/g) || [],
indexes = [],
apply = [],
tmpStr,
deltaIndex,
noCode,
final = document.createDocumentFragment(),
i;
for (i = 0, len = codes.length; i < len; i++) {
indexes.push(string.indexOf(codes[i]));
string = string.replace(codes[i], '\x00\x00');
}
if (indexes[0] !== 0) {
final.appendChild(applyCode(string.substring(0, indexes[0]), []));
}
for (i = 0; i < len; i++) {
indexDelta = indexes[i + 1] - indexes[i];
if (indexDelta === 2) {
while (indexDelta === 2) {
apply.push(codes[i]);
i++;
indexDelta = indexes[i + 1] - indexes[i];
}
apply.push(codes[i]);
} else {
apply.push(codes[i]);
}
if (apply.lastIndexOf('^r') > -1) {
apply = apply.slice(apply.lastIndexOf('^r') + 1);
}
tmpStr = string.substring(indexes[i], indexes[i + 1]);
final.appendChild(applyCode(tmpStr, apply));
}
$('#TEMPAREA').html(final);
return $('#TEMPAREA').html();;
}
function SmoothMapName(map) {
if (map != "Any Map") {
map = map.replace("/info.json", "")
map = map.split('/').pop().replace(/\s*/g, '')
map = map.replace(/_/g, " ")
map = map.replace(/-/g, " ")
map = toTitleCase(map)
}
return map
}
function toTitleCase(str) {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
var isoCountries = {
'AF': 'Afghanistan',
'AX': 'Aland Islands',
'AL': 'Albania',
'DZ': 'Algeria',
'AS': 'American Samoa',
'AD': 'Andorra',
'AO': 'Angola',
'AI': 'Anguilla',
'AQ': 'Antarctica',
'AG': 'Antigua And Barbuda',
'AR': 'Argentina',
'AM': 'Armenia',
'AW': 'Aruba',
'AU': 'Australia',
'AT': 'Austria',
'AZ': 'Azerbaijan',
'BS': 'Bahamas',
'BH': 'Bahrain',
'BD': 'Bangladesh',
'BB': 'Barbados',
'BY': 'Belarus',
'BE': 'Belgium',
'BZ': 'Belize',
'BJ': 'Benin',
'BM': 'Bermuda',
'BT': 'Bhutan',
'BO': 'Bolivia',
'BA': 'Bosnia And Herzegovina',
'BW': 'Botswana',
'BV': 'Bouvet Island',
'BR': 'Brazil',
'IO': 'British Indian Ocean Territory',
'BN': 'Brunei Darussalam',
'BG': 'Bulgaria',
'BF': 'Burkina Faso',
'BI': 'Burundi',
'KH': 'Cambodia',
'CM': 'Cameroon',
'CA': 'Canada',
'CV': 'Cape Verde',
'KY': 'Cayman Islands',
'CF': 'Central African Republic',
'TD': 'Chad',
'CL': 'Chile',
'CN': 'China',
'CX': 'Christmas Island',
'CC': 'Cocos (Keeling) Islands',
'CO': 'Colombia',
'KM': 'Comoros',
'CG': 'Congo',
'CD': 'Congo, Democratic Republic',
'CK': 'Cook Islands',
'CR': 'Costa Rica',
'CI': 'Cote D\'Ivoire',
'HR': 'Croatia',
'CU': 'Cuba',
'CY': 'Cyprus',
'CZ': 'Czech Republic',
'DK': 'Denmark',
'DJ': 'Djibouti',
'DM': 'Dominica',
'DO': 'Dominican Republic',
'EC': 'Ecuador',
'EG': 'Egypt',
'SV': 'El Salvador',
'GQ': 'Equatorial Guinea',
'ER': 'Eritrea',
'EE': 'Estonia',
'ET': 'Ethiopia',
'FK': 'Falkland Islands (Malvinas)',
'FO': 'Faroe Islands',
'FJ': 'Fiji',
'FI': 'Finland',
'FR': 'France',
'GF': 'French Guiana',
'PF': 'French Polynesia',
'TF': 'French Southern Territories',
'GA': 'Gabon',
'GM': 'Gambia',
'GE': 'Georgia',
'DE': 'Germany',
'GH': 'Ghana',
'GI': 'Gibraltar',
'GR': 'Greece',
'GL': 'Greenland',
'GD': 'Grenada',
'GP': 'Guadeloupe',
'GU': 'Guam',
'GT': 'Guatemala',
'GG': 'Guernsey',
'GN': 'Guinea',
'GW': 'Guinea-Bissau',
'GY': 'Guyana',
'HT': 'Haiti',
'HM': 'Heard Island & Mcdonald Islands',
'VA': 'Holy See (Vatican City State)',
'HN': 'Honduras',
'HK': 'Hong Kong',
'HU': 'Hungary',
'IS': 'Iceland',
'IN': 'India',
'ID': 'Indonesia',
'IR': 'Iran, Islamic Republic Of',
'IQ': 'Iraq',
'IE': 'Ireland',
'IM': 'Isle Of Man',
'IL': 'Israel',
'IT': 'Italy',
'JM': 'Jamaica',
'JP': 'Japan',
'JE': 'Jersey',
'JO': 'Jordan',
'KZ': 'Kazakhstan',
'KE': 'Kenya',
'KI': 'Kiribati',
'KR': 'Korea',
'KW': 'Kuwait',
'KG': 'Kyrgyzstan',
'LA': 'Lao People\'s Democratic Republic',
'LV': 'Latvia',
'LB': 'Lebanon',
'LS': 'Lesotho',
'LR': 'Liberia',
'LY': 'Libyan Arab Jamahiriya',
'LI': 'Liechtenstein',
'LT': 'Lithuania',
'LU': 'Luxembourg',
'MO': 'Macao',
'MK': 'Macedonia',
'MG': 'Madagascar',
'MW': 'Malawi',
'MY': 'Malaysia',
'MV': 'Maldives',
'ML': 'Mali',
'MT': 'Malta',
'MH': 'Marshall Islands',
'MQ': 'Martinique',
'MR': 'Mauritania',
'MU': 'Mauritius',
'YT': 'Mayotte',
'MX': 'Mexico',
'FM': 'Micronesia, Federated States Of',
'MD': 'Moldova',
'MC': 'Monaco',
'MN': 'Mongolia',
'ME': 'Montenegro',
'MS': 'Montserrat',
'MA': 'Morocco',
'MZ': 'Mozambique',
'MM': 'Myanmar',
'NA': 'Namibia',
'NR': 'Nauru',
'NP': 'Nepal',
'NL': 'Netherlands',
'AN': 'Netherlands Antilles',
'NC': 'New Caledonia',
'NZ': 'New Zealand',
'NI': 'Nicaragua',
'NE': 'Niger',
'NG': 'Nigeria',
'NU': 'Niue',
'NF': 'Norfolk Island',
'MP': 'Northern Mariana Islands',
'NO': 'Norway',
'OM': 'Oman',
'PK': 'Pakistan',
'PW': 'Palau',
'PS': 'Palestinian Territory, Occupied',
'PA': 'Panama',
'PG': 'Papua New Guinea',
'PY': 'Paraguay',
'PE': 'Peru',
'PH': 'Philippines',
'PN': 'Pitcairn',
'PL': 'Poland',
'PT': 'Portugal',
'PR': 'Puerto Rico',
'QA': 'Qatar',
'RE': 'Reunion',
'RO': 'Romania',
'RU': 'Russian Federation',
'RW': 'Rwanda',
'BL': 'Saint Barthelemy',
'SH': 'Saint Helena',
'KN': 'Saint Kitts And Nevis',
'LC': 'Saint Lucia',
'MF': 'Saint Martin',
'PM': 'Saint Pierre And Miquelon',
'VC': 'Saint Vincent And Grenadines',
'WS': 'Samoa',
'SM': 'San Marino',
'ST': 'Sao Tome And Principe',
'SA': 'Saudi Arabia',
'SN': 'Senegal',
'RS': 'Serbia',
'SC': 'Seychelles',
'SL': 'Sierra Leone',
'SG': 'Singapore',
'SK': 'Slovakia',
'SI': 'Slovenia',
'SB': 'Solomon Islands',
'SO': 'Somalia',
'ZA': 'South Africa',
'GS': 'South Georgia And Sandwich Isl.',
'ES': 'Spain',
'LK': 'Sri Lanka',
'SD': 'Sudan',
'SR': 'Suriname',
'SJ': 'Svalbard And Jan Mayen',
'SZ': 'Swaziland',
'SE': 'Sweden',
'CH': 'Switzerland',
'SY': 'Syrian Arab Republic',
'TW': 'Taiwan',
'TJ': 'Tajikistan',
'TZ': 'Tanzania',
'TH': 'Thailand',
'TL': 'Timor-Leste',
'TG': 'Togo',
'TK': 'Tokelau',
'TO': 'Tonga',
'TT': 'Trinidad And Tobago',
'TN': 'Tunisia',
'TR': 'Turkey',
'TM': 'Turkmenistan',
'TC': 'Turks And Caicos Islands',
'TV': 'Tuvalu',
'UG': 'Uganda',
'UA': 'Ukraine',
'AE': 'United Arab Emirates',
'GB': 'United Kingdom',
'US': 'United States',
'UM': 'United States Outlying Islands',
'UY': 'Uruguay',
'UZ': 'Uzbekistan',
'VU': 'Vanuatu',
'VE': 'Venezuela',
'VN': 'Viet Nam',
'VG': 'Virgin Islands, British',
'VI': 'Virgin Islands, U.S.',
'WF': 'Wallis And Futuna',
'EH': 'Western Sahara',
'YE': 'Yemen',
'ZM': 'Zambia',
'ZW': 'Zimbabwe'
};
function getCountryName(countryCode) {
if (isoCountries.hasOwnProperty(countryCode)) {
return isoCountries[countryCode];
} else {
return countryCode;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
.text-red {
color: #c30000;
}
.font-weight-bold {
font-weight: bold;
}
div.dataTables_wrapper {
margin-top: 10px;
color: #d1ecfc;
}
div#Clients-Table_length {
margin-top: 5px;
}
label.col-md-4.control-label {
color: #a9c5d0;
}
.Print-Only {
display: none;
}

File diff suppressed because it is too large Load Diff

122
src/style.css Normal file
View File

@@ -0,0 +1,122 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.13 0.028 261.692);
--card: oklch(1 0 0);
--card-foreground: oklch(0.13 0.028 261.692);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.13 0.028 261.692);
--primary: oklch(0.21 0.034 264.665);
--primary-foreground: oklch(0.985 0.002 247.839);
--secondary: oklch(0.967 0.003 264.542);
--secondary-foreground: oklch(0.21 0.034 264.665);
--muted: oklch(0.967 0.003 264.542);
--muted-foreground: oklch(0.551 0.027 264.364);
--accent: oklch(0.967 0.003 264.542);
--accent-foreground: oklch(0.21 0.034 264.665);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.928 0.006 264.531);
--input: oklch(0.928 0.006 264.531);
--ring: oklch(0.707 0.022 261.325);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.002 247.839);
--sidebar-foreground: oklch(0.13 0.028 261.692);
--sidebar-primary: oklch(0.21 0.034 264.665);
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
--sidebar-accent: oklch(0.967 0.003 264.542);
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
--sidebar-border: oklch(0.928 0.006 264.531);
--sidebar-ring: oklch(0.707 0.022 261.325);
}
.dark {
--background: oklch(0.13 0.028 261.692);
--foreground: oklch(0.985 0.002 247.839);
--card: oklch(0.21 0.034 264.665);
--card-foreground: oklch(0.985 0.002 247.839);
--popover: oklch(0.21 0.034 264.665);
--popover-foreground: oklch(0.985 0.002 247.839);
--primary: oklch(0.928 0.006 264.531);
--primary-foreground: oklch(0.21 0.034 264.665);
--secondary: oklch(0.278 0.033 256.848);
--secondary-foreground: oklch(0.985 0.002 247.839);
--muted: oklch(0.278 0.033 256.848);
--muted-foreground: oklch(0.707 0.022 261.325);
--accent: oklch(0.278 0.033 256.848);
--accent-foreground: oklch(0.985 0.002 247.839);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.034 264.665);
--sidebar-foreground: oklch(0.985 0.002 247.839);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
--sidebar-accent: oklch(0.278 0.033 256.848);
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

182
src/views/About.vue Normal file
View File

@@ -0,0 +1,182 @@
<script setup></script>
<template>
<section class="max-w-6xl mx-auto px-4 py-10">
<!-- Page Title -->
<div class="mb-8">
<h1 class="text-4xl font-extrabold tracking-tight text-neutral-900 dark:text-white">
About BeamMP
</h1>
<p class="mt-3 text-neutral-600 dark:text-neutral-300 max-w-2xl">
BeamMP brings multiplayer to BeamNG.drive. Its built by and for the community, focusing on
stability, performance, and an authentic driving experience shared with friends.
</p>
</div>
<!-- Creator's Note -->
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/60 p-6 mb-10"
>
<h2 class="text-2xl font-semibold text-neutral-900 dark:text-white mb-4">
A Note from the Creator
</h2>
<div class="prose prose-neutral dark:prose-invert max-w-none">
<p>
BeamMP started with a simple idea: I want to play BeamNG.drive with my brothers. What
began as an experiment quickly grew into a community-driven project focused on
implementing multiplayer, and fun. We care deeply about trying to provide the best
experience possible, and making it easy for players to hop in and enjoy driving together.
</p>
<p>
The project is open to contributors of all skill levels. Whether you write code, moderate
a server, design scenarios, or help others get set upyour efforts are part of what makes
BeamMP thrive. Thank you for being here and helping us build something special.
</p>
</div>
</div>
<!-- What is BeamMP -->
<div class="grid gap-8 md:grid-cols-2 mb-10">
<div class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-6">
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-3">
What BeamMP Provides
</h3>
<ul class="space-y-2 text-neutral-700 dark:text-neutral-300">
<li>Multiplayer sessions for BeamNG.drive with server browser and filters</li>
<li>Server-side moderation tools and configuration options</li>
<li>Mod support with protection for paid content</li>
<li>Active community channels (Forum, Discord) for help and collaboration</li>
</ul>
</div>
<div class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-6">
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-3">Project Values</h3>
<ul class="space-y-2 text-neutral-700 dark:text-neutral-300">
<li>Community-first: decisions guided by real player/server needs</li>
<li>
Reliability: prioritizing stability, sync fidelity, and performance where possible
</li>
<li>Openness: welcoming contributions, feedback, and transparency</li>
<li>Safety: promoting fair play and respectful interactions</li>
</ul>
</div>
</div>
<!-- How to Get Involved -->
<div class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-6 mb-10">
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-3">Get Involved</h3>
<p class="text-neutral-700 dark:text-neutral-300 mb-4">
There are many ways to participatejoin discussions, report issues, contribute code, or help
to support the project financially via
<a
href="https://www.patreon.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="text-orange-500 hover:underline"
>Patreon</a
>.
</p>
<div class="flex flex-wrap gap-3">
<a
href="https://forum.beammp.com"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700 transition-colors"
>Forum</a
>
<a
href="https://docs.beammp.com"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700 transition-colors"
>Docs</a
>
<a
href="https://github.com/BeamMP/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700 transition-colors"
>GitHub</a
>
<a
href="https://discord.gg/beammp"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700 transition-colors"
>Discord</a
>
<a
href="https://www.patreon.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-orange-500 text-white hover:bg-orange-600 transition-colors"
>
Patreon
</a>
</div>
</div>
<!-- Funding & Sustainability -->
<div class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-6 mb-10">
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-3">
Funding &amp; Sustainability
</h3>
<p class="text-neutral-700 dark:text-neutral-300 mb-4">
BeamMP relies on community support. Donations help cover infrastructure, bandwidth, tooling,
and development time. If you value the project and want to help it grow, please consider
supporting us.
</p>
<div class="flex flex-wrap gap-3">
<a
href="https://www.patreon.com/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-orange-500 text-white hover:bg-orange-600 transition-colors"
>
Support on Patreon
</a>
<a
href="https://github.com/BeamMP/BeamMP"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-md bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700 transition-colors"
>
Learn more on GitHub
</a>
</div>
</div>
<!-- Credits & Acknowledgements -->
<div class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-6">
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-3">
Credits &amp; Acknowledgements
</h3>
<p class="text-neutral-700 dark:text-neutral-300">
BeamMP is maintained by the Mod Team and an incredible group of community contributors. Wed
also like to thank server owners, mod creators, testers, and everyone reporting issuesyour
time and passion keep the project moving forward.
</p>
<p class="text-neutral-700 dark:text-neutral-300 mt-4">
Special thanks to the BeamNG.drive developers for creating such an amazing platform that
makes projects like BeamMP possible in the first place.
</p>
<p class="text-neutral-700 dark:text-neutral-300 mt-4">
Also a heartfelt thank you to the following current and former community members for their
significant contributions to BeamMP over the years:
</p>
<ul class="list-disc list-inside text-neutral-700 dark:text-neutral-300 mt-2 space-y-1">
<li>
Jojos38 (Co-Founder) - for their work early on with designing everything to help make the
idea become a reality
</li>
<li>Jetta (jetta.cat) - for the logo design and creation.</li>
<li>Anonymous275 & Lionkor - for their work in rebuilding the project in C++.</li>
<li>Tixx - for their considerable contributions across the project codebase.</li>
<li>And many others!</li>
</ul>
<p class="text-neutral-700 dark:text-neutral-300 mt-4">
Together, were building something special. Thank you for being part of the BeamMP
community!
</p>
</div>
</section>
</template>

298
src/views/Communities.vue Normal file
View File

@@ -0,0 +1,298 @@
<script setup>
import { ExternalLink, Users, Trophy, Gamepad2, Shield, MapPin } from 'lucide-vue-next'
const communities = [
{
name: 'BeamMP Racing League',
description:
'Competitive racing with weekly tournaments, leaderboards, and professional organization. Join racers from around the world in sanctioned events.',
image: '/beammpservers.png', // Placeholder - replace with actual community images
category: 'Racing',
icon: Trophy,
stats: {
members: '2,500+',
servers: '12',
region: 'Global',
},
features: ['Weekly tournaments', 'ELO ranking system', 'Custom race tracks', 'Prize pools'],
links: {
website: 'https://forum.beammp.com',
discord: 'https://discord.gg/beammp',
},
color: 'from-red-500 to-orange-500',
},
{
name: 'CityRP Network',
description:
'Immersive roleplay servers featuring police, emergency services, and civilian roles. Experience realistic city life with custom jobs and economy.',
image: '/beammpservers.png',
category: 'Roleplay',
icon: Gamepad2,
stats: {
members: '5,000+',
servers: '8',
region: 'NA & EU',
},
features: [
'Custom economy system',
'Police & EMS roles',
'Business ownership',
'Active moderation',
],
links: {
website: 'https://forum.beammp.com',
discord: 'https://discord.gg/beammp',
},
color: 'from-blue-500 to-cyan-500',
},
{
name: 'Destruction Derby Hub',
description:
'Pure chaos and destruction! Join epic demolition derbies, crash competitions, and vehicle destruction events with custom arenas.',
image: '/beammpservers.png',
category: 'Derby',
icon: Shield,
stats: {
members: '1,800+',
servers: '6',
region: 'Global',
},
features: ['Custom derby arenas', 'Damage scoring system', 'Weekly events', 'Vehicle variety'],
links: {
website: 'https://forum.beammp.com',
discord: 'https://discord.gg/beammp',
},
color: 'from-purple-500 to-pink-500',
},
{
name: 'Free Roam Paradise',
description:
'Casual multiplayer cruising and exploration. Meet new friends, show off your vehicles, and explore maps together in a relaxed environment.',
image: '/beammpservers.png',
category: 'Free Roam',
icon: MapPin,
stats: {
members: '3,200+',
servers: '15',
region: 'Worldwide',
},
features: ['No pressure gameplay', 'Custom vehicle packs', 'Social events', 'Map exploration'],
links: {
website: 'https://forum.beammp.com',
discord: 'https://discord.gg/beammp',
},
color: 'from-green-500 to-emerald-500',
},
{
name: 'Drift United',
description:
'Dedicated drifting community with custom drift tracks, tandem sessions, and competitions. Perfect your technique with experienced drifters.',
image: '/beammpservers.png',
category: 'Drifting',
icon: Trophy,
stats: {
members: '1,500+',
servers: '5',
region: 'Global',
},
features: ['Drift competitions', 'Tandem sessions', 'Scoring system', 'Drift tutorials'],
links: {
website: 'https://forum.beammp.com',
discord: 'https://discord.gg/beammp',
},
color: 'from-yellow-500 to-orange-500',
},
{
name: 'Offroad Expeditions',
description:
'Adventure offroad with trail runs, rock crawling, and expedition challenges. Explore rugged terrain with fellow offroad enthusiasts.',
image: '/beammpservers.png',
category: 'Offroad',
icon: MapPin,
stats: {
members: '900+',
servers: '4',
region: 'NA & EU',
},
features: ['Trail expeditions', 'Rock crawling', 'Custom terrain', 'Vehicle builds'],
links: {
website: 'https://forum.beammp.com',
discord: 'https://discord.gg/beammp',
},
color: 'from-amber-600 to-yellow-600',
},
]
</script>
<template>
<div class="space-y-16">
<!-- Page Header -->
<section class="text-center space-y-4">
<h1 class="text-5xl md:text-6xl font-bold">
<span class="text-beammp-orange">BeamMP</span> Communities
</h1>
<p class="text-xl text-neutral-600 dark:text-neutral-400 max-w-3xl mx-auto">
Discover thriving communities built around BeamMP. From competitive racing leagues to casual
free roam sessions, find your perfect group to play with.
</p>
</section>
<!-- Communities Grid -->
<section class="space-y-8">
<div
v-for="(community, index) in communities"
:key="index"
:style="{ animationDelay: `${index * 100}ms` }"
class="group bg-white dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden hover:shadow-2xl transition-all animate-fade-in-up"
>
<div class="grid md:grid-cols-5 gap-6">
<!-- Community Image -->
<div class="md:col-span-2 relative overflow-hidden">
<div :class="`absolute inset-0 bg-linear-to-br ${community.color} opacity-20`" />
<img
:src="community.image"
:alt="community.name"
class="w-full h-64 md:h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div class="absolute top-4 left-4">
<span
class="bg-black/70 backdrop-blur-sm text-white px-3 py-1 rounded-full text-sm font-semibold"
>
{{ community.category }}
</span>
</div>
</div>
<!-- Community Info -->
<div class="md:col-span-3 p-6 md:p-8 space-y-6">
<!-- Header -->
<div class="flex items-start justify-between gap-4">
<div class="flex items-center gap-3">
<component :is="community.icon" class="w-10 h-10 text-beammp-orange shrink-0" />
<div>
<h2 class="text-2xl md:text-3xl font-bold">{{ community.name }}</h2>
<p class="text-neutral-600 dark:text-neutral-400 mt-1">
{{ community.description }}
</p>
</div>
</div>
</div>
<!-- Stats -->
<div class="grid grid-cols-3 gap-4">
<div class="text-center p-3 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg">
<Users class="w-5 h-5 mx-auto mb-1 text-beammp-blue dark:text-blue-400" />
<div class="font-bold">{{ community.stats.members }}</div>
<div class="text-xs text-neutral-600 dark:text-neutral-400">Members</div>
</div>
<div class="text-center p-3 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg">
<component :is="Shield" class="w-5 h-5 mx-auto mb-1 text-beammp-orange" />
<div class="font-bold">{{ community.stats.servers }}</div>
<div class="text-xs text-neutral-600 dark:text-neutral-400">Servers</div>
</div>
<div class="text-center p-3 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg">
<MapPin class="w-5 h-5 mx-auto mb-1 text-beammp-green dark:text-green-400" />
<div class="font-bold">{{ community.stats.region }}</div>
<div class="text-xs text-neutral-600 dark:text-neutral-400">Region</div>
</div>
</div>
<!-- Features -->
<div>
<h3
class="font-semibold mb-3 text-sm uppercase tracking-wide text-neutral-600 dark:text-neutral-400"
>
Features
</h3>
<ul class="grid grid-cols-2 gap-2">
<li
v-for="(feature, idx) in community.features"
:key="idx"
class="flex items-center gap-2 text-sm"
>
<div class="w-1.5 h-1.5 rounded-full bg-beammp-orange" />
{{ feature }}
</li>
</ul>
</div>
<!-- Links -->
<div class="flex gap-3 pt-2">
<a
v-if="community.links.website"
:href="community.links.website"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 bg-beammp-blue hover:bg-beammp-blue/90 text-white px-4 py-2 rounded-lg font-semibold transition-colors text-sm"
>
Visit Website
<ExternalLink class="w-4 h-4" />
</a>
<a
v-if="community.links.discord"
:href="community.links.discord"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 bg-neutral-700 hover:bg-neutral-600 text-white px-4 py-2 rounded-lg font-semibold transition-colors text-sm"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
/>
</svg>
Join Discord
</a>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="text-center py-16 bg-neutral-50 dark:bg-neutral-900/30 -mx-4 px-4 rounded-xl">
<div class="max-w-2xl mx-auto space-y-6">
<h2 class="text-3xl md:text-4xl font-bold">Want to Start Your Own Community?</h2>
<p class="text-lg text-neutral-600 dark:text-neutral-400">
Host your own BeamMP server and build a community around your favorite game modes
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center pt-4">
<a
href="https://docs.beammp.com/server/create-a-server/"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 bg-beammp-orange hover:bg-beammp-orange/90 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
>
<component :is="Shield" class="w-5 h-5" />
Server Setup Guide
</a>
<a
href="https://discord.gg/beammp"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 bg-neutral-700 hover:bg-neutral-600 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
>
<Users class="w-5 h-5" />
Join Our Discord
</a>
</div>
</div>
</section>
</div>
</template>
<style scoped>
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out backwards;
}
</style>

11
src/views/Contact.vue Normal file
View File

@@ -0,0 +1,11 @@
<script setup></script>
<template>
<section class="space-y-4">
<h1 class="text-3xl font-bold">Contact Us</h1>
<p class="text-neutral-300 max-w-prose">
Questions, ideas, or contributions? Reach out via the official Discord or open an issue in the
repository.
</p>
</section>
</template>

502
src/views/Home.vue Normal file
View File

@@ -0,0 +1,502 @@
<script setup>
import { ref, onMounted } from 'vue'
import {
Download,
Zap,
Globe,
Server,
Package,
Code,
BookOpen,
Wrench,
Gamepad2,
Shield,
Rocket,
} from 'lucide-vue-next'
const onlinePlayers = ref('...')
const isLoading = ref(true)
onMounted(async () => {
try {
const response = await fetch('https://backend.beammp.com/metrics')
const data = await response.text()
const values = data.split(' ')
if (values.length >= 2) {
onlinePlayers.value = values[1]
}
} catch {
onlinePlayers.value = 'N/A'
} finally {
isLoading.value = false
}
})
const features = [
{
icon: Server,
title: 'Stable Servers',
description:
'BeamMP allows for stable servers, with a variety of servers located across the globe.',
},
{
icon: Package,
title: 'BeamNG.drive',
description:
"BeamMP uses the same maps, vehicles & mods so you don't need to learn anything new!",
},
{
icon: Zap,
title: 'Standalone',
description:
"BeamMP doesn't modify your original installation, so you can play either singleplayer or multiplayer.",
},
{
icon: Globe,
title: 'Sync Quality',
description:
'BeamMP updates your vehicle position ~100 times per second, allowing for a smooth overall experience.',
},
]
const communities = [
{
name: 'Racing Communities',
icon: Rocket,
description: 'Competitive racing leagues and time trials with players worldwide',
color: 'from-red-500 to-orange-500',
},
{
name: 'Roleplay Servers',
icon: Gamepad2,
description: 'Immersive roleplay experiences from police chases to delivery services',
color: 'from-blue-500 to-cyan-500',
},
{
name: 'Crash & Derby',
icon: Shield,
description: 'Demolition derbies and destruction-focused gameplay modes',
color: 'from-purple-500 to-pink-500',
},
{
name: 'Free Roam',
icon: Globe,
description: 'Casual multiplayer sessions exploring maps with friends',
color: 'from-green-500 to-emerald-500',
},
]
const devFeatures = [
{
icon: Code,
title: 'Lua API',
description: 'Powerful server-side Lua scripting for custom game modes and features',
link: 'https://docs.beammp.com/scripting/mod-reference/',
},
{
icon: BookOpen,
title: 'Documentation',
description: 'Comprehensive guides and API references for server development',
link: 'https://docs.beammp.com',
},
{
icon: Wrench,
title: 'Open Source',
description: 'Community-driven development with source available on GitHub',
link: 'https://github.com/BeamMP',
},
]
const stats = [
{ label: 'Active Players', value: onlinePlayers, suffix: '+' },
{ label: 'Global Servers', value: '500', suffix: '+' },
{ label: 'Total Downloads', value: '2M', suffix: '+' },
]
const faqs = [
{
question: 'The server list is not showing up!',
answer:
'Try restarting BeamMP as this can sometimes happen. If this fails to fix your issue please create a topic on our forum or visit the support channel on our discord.',
},
{
question: "How do I open a ticket in case something doesn't work or I have questions?",
answer:
'Please check the #how-to-use channel in Discord and on our forum. Please give an accurate description of what you have done so the support team will help you in a fast and effective way.',
},
{
question: "Help! I'm getting error codes",
answer:
'Please head over to our forum and see if anyone has had the issue before. It is likely that someone else has at some point and therefore there will also probably be a solution posted with it too in the replies. Alternatively visit our discord where the larger portion of community is currently based.',
},
{
question: 'Does this work with pirated versions of BeamNG.drive?',
answer:
"We don't know if it works with pirated versions of BeamNG.drive, but we will not provide any support to non legit copies of the game. Nor will we change to support it.",
},
{
question: 'How do I host a server?',
answer:
'The server files required for hosting your own server can be found at the top of this page below the client download. You will also require an authentication key which can be found from keymaster. Further information around the setup can be found on our wiki.',
},
{
question: 'Can I use mods?',
answer:
'Mods are supported. These are installed on the server. See our wiki for more information.',
},
]
</script>
<template>
<div class="space-y-0">
<!-- Hero Section with Background -->
<section class="relative -mt-10 mb-16 w-screen -ml-[50vw] left-1/2">
<!-- Background Image with Overlay -->
<div class="absolute inset-0 overflow-hidden">
<img
src="/beamng-mp-landing.png"
alt="BeamMP Gameplay"
class="w-full h-full object-cover"
/>
<div
class="absolute inset-0 bg-linear-to-b from-black/70 via-black/60 to-neutral-950 dark:to-neutral-950"
/>
<div class="absolute inset-0 bg-linear-to-r from-beammp-blue/20 to-beammp-orange/20" />
</div>
<!-- Hero Content -->
<div class="relative max-w-6xl mx-auto px-4 py-32 md:py-40">
<div class="max-w-4xl mx-auto text-center space-y-8 animate-fade-in">
<h1 class="text-5xl md:text-7xl font-bold leading-tight text-white">
Multiplayer for <span class="text-beammp-orange">BeamNG.drive</span>
</h1>
<p class="text-xl md:text-2xl text-neutral-200">
Experience the ultimate soft-body physics with friends. Race, roleplay, or just cruise
together.
</p>
<!-- CTA Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center pt-4">
<a
href="/installer/BeamMP_Installer.zip"
download
class="group flex items-center gap-3 bg-linear-to-r from-beammp-orange to-red-600 hover:from-red-600 hover:to-beammp-orange px-8 py-4 rounded-lg font-semibold text-lg transition-all transform hover:scale-105 shadow-lg hover:shadow-beammp-orange/50"
>
<Download class="w-6 h-6" />
Download Now
</a>
<a
href="/servers"
class="group flex items-center gap-3 bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/30 px-8 py-4 rounded-lg font-semibold text-lg transition-all text-white"
>
<Server class="w-6 h-6" />
Browse Servers
</a>
</div>
<!-- Stats -->
<div class="grid grid-cols-3 gap-8 max-w-3xl mx-auto pt-12">
<div
v-for="(stat, index) in stats"
:key="index"
class="text-center animate-fade-in-up"
:style="{ animationDelay: `${index * 100}ms` }"
>
<div class="text-3xl md:text-4xl font-bold text-beammp-orange">
<span :class="{ 'animate-pulse': stat.value === onlinePlayers && isLoading }">
{{ stat.value }} </span
>{{ stat.suffix }}
</div>
<div class="text-sm md:text-base text-neutral-300 mt-1">{{ stat.label }}</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-16 px-4">
<div class="max-w-6xl mx-auto">
<h2 class="text-4xl font-bold text-center mb-12">Why Choose BeamMP?</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
<div
v-for="(feature, index) in features"
:key="index"
:style="{ animationDelay: `${index * 100}ms` }"
class="group bg-white/70 border border-neutral-200 rounded-xl p-6 hover:border-beammp-blue hover:shadow-xl transition-all animate-fade-in-up dark:bg-neutral-900/50 dark:border-neutral-800 dark:hover:border-beammp-orange"
>
<component
:is="feature.icon"
class="w-10 h-10 text-beammp-blue mb-4 group-hover:scale-110 transition-transform dark:text-blue-400"
/>
<h3 class="text-xl font-semibold mb-2">{{ feature.title }}</h3>
<p class="text-neutral-600 dark:text-neutral-400 text-sm">{{ feature.description }}</p>
</div>
</div>
</div>
</section>
<!-- Community Types Section -->
<section class="py-16 px-4 bg-neutral-50 dark:bg-neutral-900/30">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Join a Thriving Community</h2>
<p class="text-lg text-neutral-600 dark:text-neutral-400">
Discover diverse gameplay experiences across hundreds of unique servers
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
<div
v-for="(community, index) in communities"
:key="index"
:style="{ animationDelay: `${index * 100}ms` }"
class="group relative bg-white dark:bg-neutral-900 rounded-xl p-6 overflow-hidden hover:shadow-2xl transition-all animate-fade-in-up border border-neutral-200 dark:border-neutral-800"
>
<div
:class="`absolute inset-0 bg-linear-to-br ${community.color} opacity-0 group-hover:opacity-10 transition-opacity`"
/>
<component
:is="community.icon"
class="w-12 h-12 mb-4 text-neutral-700 dark:text-neutral-300 group-hover:scale-110 transition-transform"
/>
<h3 class="text-xl font-bold mb-2">{{ community.name }}</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400">
{{ community.description }}
</p>
</div>
</div>
</div>
</section>
<!-- Server Showcase -->
<section class="py-16 px-4">
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-12 items-center">
<div class="space-y-6 animate-fade-in order-2 md:order-1">
<h2 class="text-4xl font-bold">Find Your Perfect Server</h2>
<p class="text-lg text-neutral-600 dark:text-neutral-400">
Browse hundreds of active servers with different game modes, mods, and communities. From
competitive racing to casual free roam, there's something for everyone.
</p>
<ul class="space-y-4">
<li class="flex items-start gap-3">
<div
class="w-6 h-6 rounded-full bg-beammp-blue/20 dark:bg-beammp-blue/30 flex items-center justify-center shrink-0 mt-0.5"
>
<div class="w-2 h-2 bg-beammp-blue rounded-full" />
</div>
<div>
<div class="font-semibold">Custom Game Modes</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400">
Unique experiences created by the community
</div>
</div>
</li>
<li class="flex items-start gap-3">
<div
class="w-6 h-6 rounded-full bg-beammp-orange/20 dark:bg-beammp-orange/30 flex items-center justify-center shrink-0 mt-0.5"
>
<div class="w-2 h-2 bg-beammp-orange rounded-full" />
</div>
<div>
<div class="font-semibold">Active Moderation</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400">
Safe and friendly gaming environment
</div>
</div>
</li>
<li class="flex items-start gap-3">
<div
class="w-6 h-6 rounded-full bg-beammp-green/20 dark:bg-beammp-green/30 flex items-center justify-center shrink-0 mt-0.5"
>
<div class="w-2 h-2 bg-beammp-green rounded-full" />
</div>
<div>
<div class="font-semibold">Global Network</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400">
Servers in every region for low latency
</div>
</div>
</li>
</ul>
<a
href="/servers"
class="inline-flex items-center gap-2 bg-beammp-blue hover:bg-beammp-blue/90 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
>
Browse All Servers
<Server class="w-5 h-5" />
</a>
</div>
<div
class="rounded-xl overflow-hidden border border-neutral-200 dark:border-neutral-800 shadow-2xl animate-slide-in order-1 md:order-2"
>
<img src="/beammpservers.png" alt="BeamMP Server Browser" class="w-full h-auto" />
</div>
</div>
</section>
<!-- Developer Section -->
<section class="py-16 px-4 bg-neutral-50 dark:bg-neutral-900/30">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Built for Developers</h2>
<p class="text-lg text-neutral-600 dark:text-neutral-400">
Create custom game modes, host your own server, and contribute to the project
</p>
</div>
<div class="grid md:grid-cols-3 gap-8">
<a
v-for="(dev, index) in devFeatures"
:key="index"
:href="dev.link"
target="_blank"
rel="noopener noreferrer"
:style="{ animationDelay: `${index * 100}ms` }"
class="group bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-8 hover:border-beammp-orange hover:shadow-xl transition-all animate-fade-in-up"
>
<component
:is="dev.icon"
class="w-12 h-12 text-beammp-orange mb-4 group-hover:scale-110 transition-transform"
/>
<h3 class="text-2xl font-bold mb-3">{{ dev.title }}</h3>
<p class="text-neutral-600 dark:text-neutral-400">{{ dev.description }}</p>
<div
class="mt-4 text-beammp-blue dark:text-beammp-orange font-semibold flex items-center gap-2"
>
Learn More
<svg
class="w-4 h-4 group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</a>
</div>
<!-- Download Server Section -->
<div
class="mt-12 bg-linear-to-br from-neutral-800 to-neutral-900 dark:from-neutral-950 dark:to-neutral-900 rounded-xl p-8 md:p-12 text-white"
>
<div class="max-w-3xl mx-auto text-center space-y-6">
<h3 class="text-3xl font-bold">Ready to Host Your Own Server?</h3>
<p class="text-neutral-300">
Download the server files and create your own unique BeamMP experience
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center pt-4">
<a
href="https://github.com/BeamMP/BeamMP-Server/releases/latest/download/BeamMP-Server.exe"
class="flex items-center justify-center gap-3 bg-beammp-orange hover:bg-beammp-orange/90 px-6 py-3 rounded-lg font-semibold transition-all"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"
/>
</svg>
Windows Server
<Download class="w-5 h-5" />
</a>
<a
href="https://github.com/BeamMP/BeamMP-Server/releases/latest"
class="flex items-center justify-center gap-3 bg-white/10 hover:bg-white/20 border border-white/30 px-6 py-3 rounded-lg font-semibold transition-all"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.84-.41 1.74-.348 2.642.893 13.117 23.57 6.98 21.94-5.859-.932-7.344-4.59-9.256-7.094-9.547-.31-.036-.623-.05-.935-.05-.007 0-.015 0-.023 0-.007 0-.015 0-.023 0z"
/>
</svg>
Linux Builds
<Download class="w-5 h-5" />
</a>
</div>
</div>
</div>
</div>
</section>
<!-- FAQ Section -->
<section class="space-y-8 py-8">
<h2 class="text-4xl font-bold text-center mb-12">FAQ</h2>
<div class="space-y-6 max-w-4xl mx-auto">
<details
v-for="(faq, index) in faqs"
:key="index"
class="group bg-white/70 border border-neutral-200 rounded-lg p-6 hover:border-beammp-blue/50 transition-all animate-fade-in-up dark:bg-neutral-900/50 dark:border-neutral-800"
:style="{ animationDelay: `${index * 50}ms` }"
>
<summary
class="text-lg font-semibold cursor-pointer list-none flex items-center justify-between"
>
<span>{{ faq.question }}</span>
<svg
class="w-5 h-5 transition-transform group-open:rotate-180"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</summary>
<p class="mt-4 text-neutral-600 dark:text-neutral-400">{{ faq.answer }}</p>
</details>
</div>
</section>
</div>
</template>
<style scoped>
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.6s ease-out;
}
.animate-slide-in {
animation: slide-in 0.8s ease-out;
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out backwards;
}
</style>

37
src/views/NotFound.vue Normal file
View File

@@ -0,0 +1,37 @@
<script setup>
import { RouterLink } from 'vue-router'
</script>
<template>
<section class="min-h-[60vh] flex items-center justify-center">
<div
class="text-center px-6 py-10 sm:px-8 sm:py-12 rounded-xl border border-neutral-200/30 bg-white/60 dark:bg-neutral-900/60 backdrop-blur-md shadow-sm space-y-4"
>
<div class="flex items-center justify-center gap-3">
<span
class="inline-flex items-center justify-center rounded-full bg-orange-600/10 text-orange-600 dark:text-orange-400 w-12 h-12 text-xl font-bold"
>404</span
>
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight">Page Not Found</h1>
</div>
<p class="text-neutral-600 dark:text-neutral-300 max-w-prose mx-auto">
The page you requested doesnt exist. It may have been moved or removed.
</p>
<div class="flex flex-wrap items-center justify-center gap-3 pt-2">
<RouterLink
to="/"
class="inline-flex px-4 py-2 rounded-md bg-orange-600 hover:bg-orange-500 text-white text-sm font-medium"
>
Return Home
</RouterLink>
<RouterLink
to="/servers"
class="inline-flex px-4 py-2 rounded-md border border-neutral-300/60 dark:border-neutral-700/60 text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
Browse Servers
</RouterLink>
</div>
</div>
</section>
</template>

1201
src/views/Servers.vue Normal file

File diff suppressed because it is too large Load Diff

697
src/views/Statistics.vue Normal file
View File

@@ -0,0 +1,697 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
const loading = ref(true)
const error = ref(null)
const servers = ref([])
// Backend endpoints (use null to fall back to synthetic data)
const STATS_ENDPOINT = null // e.g., 'https://backend.beammp.com/stats-timeseries'
// All-time high tracking
const peakPlayers = ref(0)
const peakServers = ref(0)
// KPIs from current servers snapshot
const serverCount = computed(() => servers.value.length)
const totalPlayers = computed(() =>
servers.value.reduce((s, x) => s + (parseInt(x.players) || 0), 0)
)
const officialCount = computed(() => servers.value.filter((s) => s.official).length)
const partnerCount = computed(() => servers.value.filter((s) => s.partner).length)
const avgPlayers = computed(() =>
serverCount.value ? Math.round((totalPlayers.value / serverCount.value) * 10) / 10 : 0
)
// Time range selection for the chart (mock until backend available)
const ranges = [
{ key: '24h', label: '24h', points: 24, stepHours: 1 },
{ key: '7d', label: '7d', points: 7 * 24, stepHours: 1 },
{ key: '30d', label: '30d', points: 30, stepHours: 24 },
]
const selectedRangeKey = ref('7d')
const selectedRange = computed(() => ranges.find((r) => r.key === selectedRangeKey.value))
// Release markers (fill with real dates/labels as desired)
const releases = ref([
{ date: '2025-11-24', label: 'v4.0.0' },
{ date: '2025-11-27', label: 'v4.1.0' },
{ date: '2025-11-29', label: 'v4.2.0' },
])
// Load time-series from backend or generate synthetic
async function loadTimeSeries(points, stepHours) {
if (STATS_ENDPOINT) {
try {
const res = await fetch(STATS_ENDPOINT)
if (!res.ok) throw new Error('Stats endpoint failed')
const data = await res.json()
// Expected format: [{ timestamp: ISO string or epoch, players: number, servers: number }, ...]
return {
players: data.map((d) => ({ t: new Date(d.timestamp), v: d.players || 0 })),
servers: data.map((d) => ({ t: new Date(d.timestamp), v: d.servers || 0 })),
}
} catch (e) {
console.error('Failed to load time series, falling back to synthetic:', e)
}
}
// Fallback: generate synthetic series
return generateSeries(totalPlayers.value || 0, serverCount.value || 0, points, stepHours)
}
// Generate a synthetic series based on current players (until API exists)
function generateSeries(baseP, baseS, points, stepHours) {
const now = new Date()
const players = []
const servers = []
let vP = Math.max(0, baseP)
let vS = Math.max(0, baseS)
let maxPlayers = peakPlayers.value || baseP
let maxServers = peakServers.value || baseS
for (let i = points - 1; i >= 0; i--) {
const t = new Date(now.getTime() - i * stepHours * 3600_000)
// daily cycle + random noise
const hour = t.getHours()
const cycle = (Math.sin((hour / 24) * 2 * Math.PI) + 1) / 2 // 0..1
const noiseP = (Math.random() - 0.5) * 0.1 * baseP
const trendP = (Math.random() - 0.5) * 0.02 * baseP
vP = Math.max(0, 0.6 * baseP + 0.6 * baseP * cycle + noiseP + trendP)
const roundedP = Math.round(vP)
players.push({ t, v: roundedP })
// Server count with similar but less volatile pattern
const noiseS = (Math.random() - 0.5) * 0.08 * baseS
const trendS = (Math.random() - 0.5) * 0.015 * baseS
vS = Math.max(0, 0.65 * baseS + 0.5 * baseS * cycle + noiseS + trendS)
const roundedS = Math.round(vS)
servers.push({ t, v: roundedS })
// Track peaks (simulate historical highs)
if (roundedP > maxPlayers) maxPlayers = roundedP
if (roundedS > maxServers) maxServers = roundedS
}
// Update peaks if synthetic data exceeded them
if (maxPlayers > peakPlayers.value) peakPlayers.value = maxPlayers
if (maxServers > peakServers.value) peakServers.value = maxServers
return { players, servers }
}
const seriesPlayers = ref([])
const seriesServers = ref([])
async function rebuildSeries() {
const data = await loadTimeSeries(selectedRange.value.points, selectedRange.value.stepHours)
seriesPlayers.value = data.players
seriesServers.value = data.servers
drawPlayers()
drawServers()
}
// Fetch servers snapshot for KPIs
onMounted(async () => {
try {
const res = await fetch('/servers.json')
if (!res.ok) throw new Error('Failed to fetch servers')
servers.value = await res.json()
// Initialize peaks from current snapshot (will be updated by time series if higher)
peakPlayers.value = totalPlayers.value
peakServers.value = serverCount.value
} catch (e) {
console.error(e)
error.value = 'Failed to load statistics. Please try again later.'
} finally {
loading.value = false
await rebuildSeries()
}
window.addEventListener('resize', () => {
drawPlayers()
drawServers()
})
})
onBeforeUnmount(() => {
window.removeEventListener('resize', () => {
drawPlayers()
drawServers()
})
})
watch([selectedRangeKey, servers], async () => {
if (!loading.value) await rebuildSeries()
})
// Canvas charts (no deps)
const canvasElPlayers = ref(null)
const canvasElServers = ref(null)
const tooltipPlayers = ref({ show: false, x: 0, y: 0, text: '' })
const tooltipServers = ref({ show: false, x: 0, y: 0, text: '' })
const tooltipRelease = ref({ show: false, x: 0, y: 0, text: '', chart: '' })
let lastBoundsPlayers = { left: 0, top: 0 }
let lastBoundsServers = { left: 0, top: 0 }
let releaseMarkersPlayers = []
let releaseMarkersServers = []
function drawPlayers() {
const canvas = canvasElPlayers.value
if (!canvas) return
const parent = canvas.parentElement
const w = parent.clientWidth
const h = 320
canvas.width = w * window.devicePixelRatio
canvas.height = h * window.devicePixelRatio
canvas.style.width = w + 'px'
canvas.style.height = h + 'px'
const ctx = canvas.getContext('2d')
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
// Background
ctx.clearRect(0, 0, w, h)
const padding = { left: 48, right: 16, top: 16, bottom: 28 }
const plotW = w - padding.left - padding.right
const plotH = h - padding.top - padding.bottom
if (plotW <= 0 || plotH <= 0 || seriesPlayers.value.length === 0) return
const xs = seriesPlayers.value
const minV = 0
const maxV = Math.max(10, Math.max(...xs.map((p) => p.v)))
const minT = xs[0].t.getTime()
const maxT = xs[xs.length - 1].t.getTime()
const tx = (t) => padding.left + ((t - minT) / (maxT - minT)) * plotW
const ty = (v) => padding.top + (1 - (v - minV) / (maxV - minV)) * plotH
// Axes
ctx.strokeStyle =
getComputedStyle(document.documentElement).getPropertyValue('--border') || '#e5e7eb'
ctx.fillStyle =
getComputedStyle(document.documentElement).getPropertyValue('--foreground') || '#111827'
ctx.lineWidth = 1
// y gridlines and labels (4 steps)
ctx.font = '12px system-ui, -apple-system, Segoe UI, Roboto, Arial'
ctx.fillStyle = 'rgba(100,100,100,0.9)'
ctx.textAlign = 'right'
ctx.textBaseline = 'middle'
for (let i = 0; i <= 4; i++) {
const v = (i / 4) * maxV
const y = ty(v)
ctx.strokeStyle = 'rgba(120,120,120,0.15)'
ctx.beginPath()
ctx.moveTo(padding.left, y)
ctx.lineTo(w - padding.right, y)
ctx.stroke()
ctx.fillText(String(Math.round(v)), padding.left - 8, y)
}
// x axis baseline
ctx.strokeStyle = 'rgba(120,120,120,0.3)'
ctx.beginPath()
ctx.moveTo(padding.left, h - padding.bottom)
ctx.lineTo(w - padding.right, h - padding.bottom)
ctx.stroke()
// x axis labels (4-6 time points)
ctx.fillStyle = 'rgba(100,100,100,0.9)'
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
const numLabels = Math.min(6, Math.floor(plotW / 80))
for (let i = 0; i <= numLabels; i++) {
const t = minT + (i / numLabels) * (maxT - minT)
const x = tx(t)
const date = new Date(t)
const label =
selectedRange.value.key === '24h'
? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: date.toLocaleDateString([], { month: 'short', day: 'numeric' })
ctx.fillText(label, x, h - padding.bottom + 6)
}
// Line path
ctx.strokeStyle = '#4a89dc'
ctx.lineWidth = 2
ctx.beginPath()
xs.forEach((p, i) => {
const x = tx(p.t.getTime())
const y = ty(p.v)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
})
ctx.stroke()
// Points
ctx.fillStyle = '#4a89dc'
xs.forEach((p) => {
const x = tx(p.t.getTime())
const y = ty(p.v)
ctx.beginPath()
ctx.arc(x, y, 2.5, 0, Math.PI * 2)
ctx.fill()
})
// Release markers
ctx.strokeStyle = 'rgba(255,106,0,0.8)'
ctx.fillStyle = 'rgba(255,106,0,0.9)'
ctx.lineWidth = 1
const rels = (releases.value || [])
.map((r) => ({ ...r, time: new Date(r.date).getTime() }))
.filter((r) => !isNaN(r.time) && r.time >= minT && r.time <= maxT)
releaseMarkersPlayers = []
rels.forEach((r) => {
const x = tx(r.time)
releaseMarkersPlayers.push({ x, label: r.label, date: r.date, time: r.time })
ctx.beginPath()
ctx.moveTo(x, padding.top)
ctx.lineTo(x, h - padding.bottom)
ctx.stroke()
// small marker triangle at top
ctx.beginPath()
ctx.moveTo(x, padding.top)
ctx.lineTo(x - 4, padding.top + 8)
ctx.lineTo(x + 4, padding.top + 8)
ctx.closePath()
ctx.fill()
})
lastBoundsPlayers = canvas.getBoundingClientRect()
}
function drawServers() {
const canvas = canvasElServers.value
if (!canvas) return
const parent = canvas.parentElement
const w = parent.clientWidth
const h = 320
canvas.width = w * window.devicePixelRatio
canvas.height = h * window.devicePixelRatio
canvas.style.width = w + 'px'
canvas.style.height = h + 'px'
const ctx = canvas.getContext('2d')
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
// Background
ctx.clearRect(0, 0, w, h)
const padding = { left: 48, right: 16, top: 16, bottom: 28 }
const plotW = w - padding.left - padding.right
const plotH = h - padding.top - padding.bottom
if (plotW <= 0 || plotH <= 0 || seriesServers.value.length === 0) return
const xs = seriesServers.value
const minV = 0
const maxV = Math.max(10, Math.max(...xs.map((p) => p.v)))
const minT = xs[0].t.getTime()
const maxT = xs[xs.length - 1].t.getTime()
const tx = (t) => padding.left + ((t - minT) / (maxT - minT)) * plotW
const ty = (v) => padding.top + (1 - (v - minV) / (maxV - minV)) * plotH
// Axes
ctx.strokeStyle =
getComputedStyle(document.documentElement).getPropertyValue('--border') || '#e5e7eb'
ctx.fillStyle =
getComputedStyle(document.documentElement).getPropertyValue('--foreground') || '#111827'
ctx.lineWidth = 1
// y gridlines and labels (4 steps)
ctx.font = '12px system-ui, -apple-system, Segoe UI, Roboto, Arial'
ctx.fillStyle = 'rgba(100,100,100,0.9)'
ctx.textAlign = 'right'
ctx.textBaseline = 'middle'
for (let i = 0; i <= 4; i++) {
const v = (i / 4) * maxV
const y = ty(v)
ctx.strokeStyle = 'rgba(120,120,120,0.15)'
ctx.beginPath()
ctx.moveTo(padding.left, y)
ctx.lineTo(w - padding.right, y)
ctx.stroke()
ctx.fillText(String(Math.round(v)), padding.left - 8, y)
}
// x axis baseline
ctx.strokeStyle = 'rgba(120,120,120,0.3)'
ctx.beginPath()
ctx.moveTo(padding.left, h - padding.bottom)
ctx.lineTo(w - padding.right, h - padding.bottom)
ctx.stroke()
// x axis labels (4-6 time points)
ctx.fillStyle = 'rgba(100,100,100,0.9)'
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
const numLabels = Math.min(6, Math.floor(plotW / 80))
for (let i = 0; i <= numLabels; i++) {
const t = minT + (i / numLabels) * (maxT - minT)
const x = tx(t)
const date = new Date(t)
const label =
selectedRange.value.key === '24h'
? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: date.toLocaleDateString([], { month: 'short', day: 'numeric' })
ctx.fillText(label, x, h - padding.bottom + 6)
}
// Line path (different color for servers)
ctx.strokeStyle = '#22c55e'
ctx.lineWidth = 2
ctx.beginPath()
xs.forEach((p, i) => {
const x = tx(p.t.getTime())
const y = ty(p.v)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
})
ctx.stroke()
// Points
ctx.fillStyle = '#22c55e'
xs.forEach((p) => {
const x = tx(p.t.getTime())
const y = ty(p.v)
ctx.beginPath()
ctx.arc(x, y, 2.5, 0, Math.PI * 2)
ctx.fill()
})
// Release markers
ctx.strokeStyle = 'rgba(255,106,0,0.8)'
ctx.fillStyle = 'rgba(255,106,0,0.9)'
ctx.lineWidth = 1
const rels = (releases.value || [])
.map((r) => ({ ...r, time: new Date(r.date).getTime() }))
.filter((r) => !isNaN(r.time) && r.time >= minT && r.time <= maxT)
releaseMarkersServers = []
rels.forEach((r) => {
const x = tx(r.time)
releaseMarkersServers.push({ x, label: r.label, date: r.date, time: r.time })
ctx.beginPath()
ctx.moveTo(x, padding.top)
ctx.lineTo(x, h - padding.bottom)
ctx.stroke()
// small marker triangle at top
ctx.beginPath()
ctx.moveTo(x, padding.top)
ctx.lineTo(x - 4, padding.top + 8)
ctx.lineTo(x + 4, padding.top + 8)
ctx.closePath()
ctx.fill()
})
lastBoundsServers = canvas.getBoundingClientRect()
}
function onMovePlayers(e) {
const canvas = canvasElPlayers.value
if (!canvas || seriesPlayers.value.length === 0) return
const rect = lastBoundsPlayers
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// Check if hovering over a release marker (priority over data points)
for (const marker of releaseMarkersPlayers) {
if (Math.abs(marker.x - x) < 8) {
tooltipRelease.value = {
show: true,
x: marker.x,
y: 40,
text: `${marker.label}\n${new Date(marker.date).toLocaleDateString()}`,
chart: 'players',
}
tooltipPlayers.value.show = false
return
}
}
tooltipRelease.value.show = false
// find nearest point by x distance
const xs = seriesPlayers.value
const minT = xs[0].t.getTime()
const maxT = xs[xs.length - 1].t.getTime()
const w = canvas.clientWidth
const padding = { left: 48, right: 16 }
const plotW = w - padding.left - padding.right
const tx = (t) => padding.left + ((t - minT) / (maxT - minT)) * plotW
let nearest = null,
best = Infinity
xs.forEach((p) => {
const px = tx(p.t.getTime())
const d = Math.abs(px - x)
if (d < best) {
best = d
nearest = { px, p }
}
})
if (nearest && best < 24) {
tooltipPlayers.value = {
show: true,
x: nearest.px,
y,
text: `${nearest.p.v} players\n${nearest.p.t.toLocaleString()}`,
}
} else {
tooltipPlayers.value.show = false
}
}
function onMoveServers(e) {
const canvas = canvasElServers.value
if (!canvas || seriesServers.value.length === 0) return
const rect = lastBoundsServers
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// Check if hovering over a release marker (priority over data points)
for (const marker of releaseMarkersServers) {
if (Math.abs(marker.x - x) < 8) {
tooltipRelease.value = {
show: true,
x: marker.x,
y: 40,
text: `${marker.label}\n${new Date(marker.date).toLocaleDateString()}`,
chart: 'servers',
}
tooltipServers.value.show = false
return
}
}
tooltipRelease.value.show = false
// find nearest point by x distance
const xs = seriesServers.value
const minT = xs[0].t.getTime()
const maxT = xs[xs.length - 1].t.getTime()
const w = canvas.clientWidth
const padding = { left: 48, right: 16 }
const plotW = w - padding.left - padding.right
const tx = (t) => padding.left + ((t - minT) / (maxT - minT)) * plotW
let nearest = null,
best = Infinity
xs.forEach((p) => {
const px = tx(p.t.getTime())
const d = Math.abs(px - x)
if (d < best) {
best = d
nearest = { px, p }
}
})
if (nearest && best < 24) {
tooltipServers.value = {
show: true,
x: nearest.px,
y,
text: `${nearest.p.v} servers\n${nearest.p.t.toLocaleString()}`,
}
} else {
tooltipServers.value.show = false
}
}
function onLeavePlayers() {
tooltipPlayers.value.show = false
if (tooltipRelease.value.chart === 'players') tooltipRelease.value.show = false
}
function onLeaveServers() {
tooltipServers.value.show = false
if (tooltipRelease.value.chart === 'servers') tooltipRelease.value.show = false
}
</script>
<template>
<section class="max-w-6xl mx-auto px-4 py-10">
<div class="mb-8">
<h1 class="text-4xl font-extrabold tracking-tight text-neutral-900 dark:text-white">
BeamMP Statistics
</h1>
<p class="mt-3 text-neutral-600 dark:text-neutral-300 max-w-2xl">
Live snapshot metrics from active servers and a player volume timeline. Releases can be
annotated on the chart.
</p>
</div>
<div v-if="loading" class="py-16 text-center text-neutral-600 dark:text-neutral-300">
Loading
</div>
<div v-else-if="error" class="py-16 text-center text-red-600">{{ error }}</div>
<div v-else class="space-y-8">
<!-- KPI Cards -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-4 bg-white/70 dark:bg-neutral-900/60"
>
<div class="text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Current Players
</div>
<div class="mt-1 text-3xl font-semibold">{{ totalPlayers }}</div>
<div class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
Peak: {{ peakPlayers }}
</div>
</div>
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-4 bg-white/70 dark:bg-neutral-900/60"
>
<div class="text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Current Servers
</div>
<div class="mt-1 text-3xl font-semibold">{{ serverCount }}</div>
<div class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
Peak: {{ peakServers }}
</div>
</div>
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-4 bg-white/70 dark:bg-neutral-900/60"
>
<div class="text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Avg Players / Server
</div>
<div class="mt-1 text-3xl font-semibold">{{ avgPlayers }}</div>
</div>
</div>
<!-- Secondary Stats -->
<div class="grid grid-cols-2 gap-4">
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-3 bg-white/70 dark:bg-neutral-900/60"
>
<div class="text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Official Servers
</div>
<div class="mt-1 text-2xl font-semibold">{{ officialCount }}</div>
</div>
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-3 bg-white/70 dark:bg-neutral-900/60"
>
<div class="text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Partner Servers
</div>
<div class="mt-1 text-2xl font-semibold">{{ partnerCount }}</div>
</div>
</div>
<!-- Player Volume Chart -->
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-4 bg-white/70 dark:bg-neutral-900/60"
>
<div class="flex items-center justify-between gap-4 mb-3">
<h2 class="text-lg font-semibold">Player Volume Over Time</h2>
<div class="flex items-center gap-2">
<button
v-for="r in ranges"
:key="r.key"
class="px-3 py-1.5 rounded-md text-sm border transition-colors"
:class="
r.key === selectedRangeKey
? 'bg-neutral-900 text-white dark:bg-white dark:text-neutral-900 border-transparent'
: 'bg-transparent text-neutral-700 dark:text-neutral-300 border-neutral-300 dark:border-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-800'
"
@click="selectedRangeKey = r.key"
>
{{ r.label }}
</button>
</div>
</div>
<div class="relative">
<canvas
ref="canvasElPlayers"
class="w-full h-80"
@mousemove="onMovePlayers"
@mouseleave="onLeavePlayers"
/>
<div
v-if="tooltipPlayers.show"
class="pointer-events-none absolute px-2 py-1 rounded bg-neutral-900 text-white text-xs whitespace-pre"
:style="{
left: tooltipPlayers.x + 'px',
top: tooltipPlayers.y + 'px',
transform: 'translate(-50%, -120%)',
}"
>
{{ tooltipPlayers.text }}
</div>
<div
v-if="tooltipRelease.show && tooltipRelease.chart === 'players'"
class="pointer-events-none absolute px-2 py-1 rounded bg-orange-600 text-white text-xs whitespace-pre font-semibold"
:style="{
left: tooltipRelease.x + 'px',
top: tooltipRelease.y + 'px',
transform: 'translate(-50%, 0)',
}"
>
{{ tooltipRelease.text }}
</div>
</div>
<p class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Release markers are shown as orange vertical lines (e.g., v3.0.0, v4.0.0). Hover over data
points for details.
</p>
</div>
<!-- Server Count Chart -->
<div
class="rounded-lg border border-neutral-200 dark:border-neutral-800 p-4 bg-white/70 dark:bg-neutral-900/60"
>
<div class="flex items-center justify-between gap-4 mb-3">
<h2 class="text-lg font-semibold">Server Count Over Time</h2>
</div>
<div class="relative">
<canvas
ref="canvasElServers"
class="w-full h-80"
@mousemove="onMoveServers"
@mouseleave="onLeaveServers"
/>
<div
v-if="tooltipServers.show"
class="pointer-events-none absolute px-2 py-1 rounded bg-neutral-900 text-white text-xs whitespace-pre"
:style="{
left: tooltipServers.x + 'px',
top: tooltipServers.y + 'px',
transform: 'translate(-50%, -120%)',
}"
>
{{ tooltipServers.text }}
</div>
<div
v-if="tooltipRelease.show && tooltipRelease.chart === 'servers'"
class="pointer-events-none absolute px-2 py-1 rounded bg-orange-600 text-white text-xs whitespace-pre font-semibold"
:style="{
left: tooltipRelease.x + 'px',
top: tooltipRelease.y + 'px',
transform: 'translate(-50%, 0)',
}"
>
{{ tooltipRelease.text }}
</div>
</div>
<p class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Server count follows similar trends. Release markers indicate major version launches.
</p>
</div>
</div>
</section>
</template>

View File

@@ -1,46 +0,0 @@
<footer class="py-5">
<div class="container">
<div class="row">
<div class="col-6 col-sm-4 text-xs-center order-sm-2">
<a class="text-white" target="_blank" href="https://www.reddit.com/r/BeamMP">
<i class="fab fa-reddit"></i>
</a>
<a class="text-white" target="_blank" href="https://twitter.com/BeamMP_Mod_Team">
<i class="fab fa-twitter"></i>
</a>
<a class="text-white" target="_blank" href="https://www.facebook.com/BeamNGMP">
<i class="fab fa-facebook"></i>
</a>
</div>
<div class="col-6 justify-content-between col-sm-4 text-right text-xs-center order-sm-3">
<a class="text-white" target="_blank" href="https://github.com/BeamMP">
<i class="fab fa-github"></i>
</a>
<a class="text-white" target="_blank" href="https://discord.gg/beammp">
<i class="fab fa-discord"></i>
</a>
<a class="text-white" target="_blank" href="https://www.beamng.com/threads/beamng-drive-multiplayer-beamng-mp-formally-local-multiplayer-lua-based.63052/">
<i class="fa fa-envelope"></i>
</a>
</div>
</div>
<div class="col-12 col-sm-4 text-center py-4 order-sm-2">
<small class="text-white">&#xA9; 2019 - Present | BeamMP Mod Team All Rights Reserved</small>
<small class="ml-1">
<a href="https://forum.beammp.com/topic/95/privacy-policy-v1-0" target="_blank">Privacy Policy</a>
&middot;
<a href="https://forum.beammp.com/topic/94/terms-of-use-v1-0">Terms &amp; Conditions</a>
</small>
</div>
</div>
</footer>

View File

@@ -1,95 +0,0 @@
<div class="container">
<div class="row center-xs">
<div class="col-md-4 col-sm-4">
<a class="logo-img" href="/">
<div class="logo-image">
<img src="../img/beammp-logo.png" alt="BeamMP Logo" class="img-fluid"/>
</div>
</a>
</div>
<div class="col-md-8 col-sm-8">
<div class="burger-menu">
<button class="navbar-toggler toggler-example" type="button" data-toggle="collapse" data-target="#navbarSupportedContent1" aria-controls="navbarSupportedContent1" aria-expanded="false" aria-label="Toggle navigation"><span class="dark-blue-text"><i class="fas fa-bars fa-1x"></i></span></button>
</div>
<!-- Collapsible content -->
<div class="collapse navbar-collapse" id="navbarSupportedContent1">
<!-- Links -->
<div class="mobile-container">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="https://forum.beammp.com">Forum</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://wiki.beammp.com">Wiki</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/servers">Servers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/stats">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/BeamMP/BeamMP">GitHub</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://www.patreon.com/BeamMP">Patreon</a>
</li>
</ul>
</div>
</div>
<div class="mobile-container">
<div class="topnav mobile-links">
<div id="burger-links">
<a href="https://forum.beammp.com">Forum</a>
<a href="https://wiki.beammp.com">Docs</a>
<a href="/servers">Servers</a>
<a href="/stats">Statistics</a>
<a href="https://github.com/BeamMP/BeamMP">GitHub</a>
<a href="https://www.patreon.com/BeamMP">Patreon</a>
</div>
<a href="javascript:void(0);" class="icon" onclick="myFunction()">
<i class="fa fa-bars"></i>
</a>
</div>
</div>
<div class="navbar-links">
<ul class="links">
<li>
<a href="https://forum.beammp.com">
<span class="text">Forum</span>
</a>
</li>
<li>
<a href="https://wiki.beammp.com">
<span class="text">Docs</span>
</a>
</li>
<li>
<a href="/servers">
<span class="text">Servers</span>
</a>
</li>
<li>
<a href="/stats">
<span class="text">Statistics</span>
</a>
</li>
<li>
<a href="https://github.com/BeamMP/BeamMP">
<span class="text">Github</span>
</a>
</li>
<li>
<a href="https://www.patreon.com/BeamMP">
<span class="text">Patreon</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -1,191 +0,0 @@
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="abuseipdb-verification" content="K1hJK7Gv" />
<title>BeamMP</title>
<link rel="stylesheet" type="text/css" href="css/styles.css"/>
<!-- <link rel="stylesheet" type="text/css" href="../static/css/styles.css"/>-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
<script src="https://kit.fontawesome.com/90f05b466c.js" crossorigin="anonymous"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-160071688-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-160071688-1');
</script>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Make a Fetch request to fetch the data
fetch("https://backend.beammp.com/metrics")
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.text();
})
.then(data => {
// Split the string by space
var values = data.split(" ");
// Check if the second value exists
if (values.length >= 2) {
// Set the second value into the element with ID "ONLINE-PLAYERS"
document.getElementById("ONLINE-PLAYERS").textContent = values[1];
} else {
// Handle the case where the string does not contain enough values
document.getElementById("ONLINE-PLAYERS").textContent = "Error: Insufficient data";
}
})
.catch(error => {
// Handle errors during the Fetch request
document.getElementById("ONLINE-PLAYERS").textContent = "Error: " + error.message;
});
});
</script>
</head>
<body>
<nav class="nav">
<%- include('includes/nav.ejs') %>
</nav>
<header class="main-content" style="background-position: center center, center 0px;">
<div class="introduction" style="margin-top: 0px;">
<div class="container">
<div class="row center-xs middle-xs">
<div class="col-lg-8 col-md-8 col-sm-8 col-xs-12">
<p class="lead">
<b>BeamMP</b> Bringing Multiplayer to BeamNG.drive!<br>
With a smooth and enjoyable experience.
</div>
<div id="button" class="col-lg-4 col-md-4 col-sm-4 buttons-wrapper hidden-xs">
<div>
<h2>Players Online: <span id="ONLINE-PLAYERS"></span></h2>
</div>
<div class="buttons mt-3">
<button class="download-client js-show-story" onclick="downloadinstaller()" href="beamMP.zip" download>
<img class="os-icon" src="https://raw.githubusercontent.com/devicons/devicon/master/icons/windows8/windows8-original.svg" alt="windows-logo">
<span class="text">Download Client</span>
<span class="description"> BeamMP_Installer.zip</span>
</button>
</div>
<div class="buttons mt-3">
<form action="https://github.com/BeamMP/BeamMP-Server/releases/latest/download/BeamMP-Server.exe">
<button class="download-client js-show-story" style="background: radial-gradient(circle,rgb(255 179 38 / 80%),rgb(255 176 0 / 65%)) center/100%;">
<img class="os-icon" src="https://raw.githubusercontent.com/devicons/devicon/master/icons/windows8/windows8-original.svg" alt="windows-logo">
<span class="text">Download Server</span>
<span class="description"> BeamMP-Server.exe</span>
</button>
</form>
</div>
<div class="buttons mt-3">
<form action="https://github.com/BeamMP/BeamMP-Server/releases/latest">
<button class="download-client js-show-story" style="background: radial-gradient(circle,rgb(255 179 38 / 80%),rgb(255 176 0 / 65%)) center/100%;">
<img class="os-icon" src="https://raw.githubusercontent.com/devicons/devicon/master/icons/linux/linux-plain.svg" alt="linux-logo">
<span class="text">Download Server</span>
<span class="description">Linux builds</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<section id="features">
<div class="container">
<div class="row center-lg center-md center-sm center-xs features">
<article class="col-lg-3">
<h2>Stable Servers</h2>
BeamMP allows for stable servers,
with a variety of servers located accross the globe.
</article>
<article class="col-lg-3 col-md-6 col-sm-6 col-xs-12">
<h2> BeamNG.drive </h2> BeamMP uses the same maps, vehicles & mods
so you don't need to learn anything new!
</article>
<article class="col-lg-3 col-md-6 col-sm-6 col-xs-12">
<h2> Standalone </h2> BeamMP doesn't modify your original installation,
so you can play either singleplayer or multiplayer.
</article>
<article class="col-lg-3 col-md-6 col-sm-6 col-xs-12">
<h2> Sync quality </h2> BeamMP updates your vehicle position ~100 times per second,
allowing for a smooth overall experience.
</article>
</div>
</div>
<hr>
<div class="row middle-lg">
<div class="col-lg-6">
<figure>
<img src="../img/beammpservers.png">
</figure>
</div>
<div class="col-lg-6" style="text-align:center">
<ul class="user-features-list">
<li> Dedicated servers </li>
<li> The original BeamNG.Drive</li>
<li> Dedicated support team</li>
<li> 24/7 Global Access</li>
</ul>
</div>
</div>
</section>
<hr>
<section id="faq"><div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="container">
<!-- <div class="row"> -->
<h1>FAQ.</h1>
<div class="faq">
<h3 id="faq-heading">The server list is not showing up!</h3>
<p>Try restarting BeamMP as this can sometimes happen, if this fails to fix your issue please create a topic on our forum or visit the support channel on our discord</p>
<h3 id="faq-heading">How do I open a ticket in case something doesnt work or I have questions?</h3>
<p>Please check the <b>#how-to-use</b> channel in Discord and on our forum. Please give an accurate description of what youve done so the support team will help you in a fast and effective way. </p>
<h3 id="faq-heading">Help! Im getting error codes</h3>
<p>Please head over to our forum and see if anyone has had the issue before. It is likely that someone else has at some point and therefore there will also probably be a solution posted with it too in the replies. Altervatively visit our discord where the larger portion of community is currently based.</p>
<h3 id="faq-heading"> Does this work with pirated versions of BeamNG.drive?</h3>
<p>We dont know if it works with pirated versions of BeamNG.drive, but we <b>will not</b> provide any support to non legit copies of the game. Nor will we change to support it.</p>
<h3 id="faq-heading">How do I host a server?</h3>
<p>The server files required for hosting your own server can be found at the top of this page below the client download. You will also require a authentication key which can be found from <a href="https://keymaster.beammp.com/">keymaster</a>. Further information around the setup can be found on our <a href="https://wiki.beammp.com/en/home/Server_Mod">wiki</a>.</p>
<h3 id="faq-heading">Can I use mods?</h3>
<p>Mods are supported, These are installed on the server. See our <a href="https://wiki.beammp.com/en/home/Server_Mod">wiki</a> for more infomation.</p>
</div>
</div>
</div>
</div>
</section>
<%- include('includes/footer.ejs') %>
<script>
function downloadinstaller() {
document.location.href ="/installer/BeamMP_Installer.zip";
//alert("Sorry this is coming soon, Therefore it is not available just yet. Please join the discord to get the latest version: https://discord.gg/beammp")
}
function downloadserver() {
document.location.href ="https://github.com/BeamMP/BeamMP-Server/releases/latest";
//alert("Sorry this is coming soon, Therefore it is not available just yet. Please join the discord to get the latest version: https://discord.gg/beammp")
}
function myFunction() {
var x = document.getElementById("burger-links");
if (x.style.display === "block") {
x.style.display = "none";
} else {
x.style.display = "block";
}
}
</script>
<script src="js/script.js"></script>
<!-- <script src="../static/js/script.js"></script>-->
</body>

View File

@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>BeamMP-Servers</title>
<link rel="stylesheet" type="text/css" href="css/styles.css"/>
<link href="css/servers-styles.css" rel="stylesheet" />
<link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/js/all.min.js" crossorigin="anonymous"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-160071688-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-160071688-1');
</script>
</head>
<body class="sb-nav-fixed">
<span id="TEMPAREA" style="display:none;"></span>
<nav class="nav" style="position: unset !important;">
<%- include('includes/nav.ejs') %>
</nav>
<div class="container-fluid">
<div id="layoutSidenav_content">
<h1 class="mt-4" style="color:white;">Server List</h1>
<div class="card mb-4">
<div class="card-header"><i class="fas fa-server mr-1"></i>BeamMP Server List</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Location</th>
<th>Server Name</th>
<th>Map</th>
<th>Players</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Location</th>
<th>Server Name</th>
<th>Map</th>
<th>Players</th>
</tr>
</tfoot>
<tbody id="Servers-List">
</tbody>
</table>
</div>
</div>
</div>
</div>
<%- include('includes/footer.ejs') %>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="js/scripts.js"></script>
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js" crossorigin="anonymous"></script>
</body>
</html>

View File

@@ -1,192 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="BeamMP Mod Team" />
<title>BeamMP | Statistics</title>
<link rel="stylesheet" type="text/css" href="css/styles.css"/>
<link href="css/servers-styles.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/js/all.min.js" crossorigin="anonymous"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-160071688-1"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-160071688-1');
</script>
</head>
<body>
<nav class="nav" style="position: unset !important;">
<%- include('includes/nav.ejs') %>
</nav>
<div class="container-fluid">
<div id="layoutSidenav_content">
<div class="row">
<div class="col-md-12">
<h1 class="mt-4" style="color:white;"><i class="fas fa-server mr-1"></i>BeamMP Statistics <h3 id="LivePlayerCount" style="color:white;"></h3></h1>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Highest Recorded Players</h5>
<p class="card-text text-muted" id="MAXPLAYERS"></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Highest Recorded Servers</h5>
<p class="card-text text-muted" id="MAXSERVERS"></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Todays Average Players</h5>
<p class="card-text text-muted" id="AVERAGES"></p
</div>
</div>
</div>
</div>
<div class="col-md-12">
<canvas id="myChart" width="100vw" height="770"></canvas>
</div>
</div>
<footer class="py-4 bg-light mt-auto">
<div class="container-fluid">
<div class="d-flex align-items-center justify-content-between small">
<div class="text-muted">Copyright &copy; BeamMP Development Team 2021</div>
<div>
<a href="https://forum.beammp.com/topic/95/privacy-policy-v1-0">Privacy Policy</a>
&middot;
<a href="https://forum.beammp.com/topic/94/terms-of-use-v1-0">Terms &amp; Conditions</a>
</div>
</div>
</div>
</footer>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js" crossorigin="anonymous"></script>
<script type="text/javascript">
var chartData = {};
var GetChartData = function (period) {
$.ajax({
type: 'GET',
url: 'https://backend.beammp.com/stats-info',
data: {
period: (period) ? period : 'today',
},
dataType: 'json',
success: function (d) {
console.log(d)
var average = 0;
var Data = {}
Data.Players = []
Data.Servers = []
Data.v2Players = []
Data.v2Servers = []
Data.Labels = []
console.log(d.maxp, d.maxs)
console.log(d.v2history)
if (d.v2history) {
var count = 0
var tot = 0
d.v2history.forEach(function(item, index) {
Data.Labels.push(item.datetime)
Data.v2Players.push(item.players)
if (typeof(item.servers) == 'number') Data.v2Servers.push(item.servers);
if (typeof(item.servers) == 'object') Data.v2Servers.push(item.servers.count);
tot += parseInt(item.players)
count++;
});
average = tot / count
}
$('#MAXPLAYERS').text(d.maxp);
$('#MAXSERVERS').text(d.maxs);
$('#AVERAGES').text(Math.floor(average));
$('#LivePlayerCount').text(`Currently Online: ${d.v2history[d.v2history.length-1].players}`)
console.log(Data)
document.getElementById('myChart').innerHTML = '';
var ctx = document.getElementById('myChart').getContext('2d');
ctx.height = 770;
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: Data.Labels,
datasets: [{
label: 'Servers',
backgroundColor: 'rgba(255,0,0,0.1)',
borderColor: 'rgba(255,0,0,0.4)',
data: Data.v2Servers,
fill: false,
}, {
label: 'Players',
fill: false,
backgroundColor: 'rgba(0,0,255,0.1)',
borderColor: 'rgba(0,0,255,0.4)',
data: Data.v2Players,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: 'BeamMP Statistics'
},
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'nearest',
intersect: true
},
scales: {
x: {
display: true,
scaleLabel: {
display: true,
labelString: 'Time'
}
},
y: {
display: true,
scaleLabel: {
display: true,
labelString: 'Count'
}
}
}
}
})
}
});
};
$(document).ready(function() {
GetChartData();
});
//const socket = io('./stats');
</script>
</body>
</html>

View File

@@ -1,116 +0,0 @@
/*
* oooooooooo. ooo ooooo ooooooooo.
* `888' `Y8b `88. .888' `888 `Y88.
* 888 888 .ooooo. .oooo. ooo. .oo. .oo. 888b d'888 888 .d88'
* 888oooo888' d88' `88b `P )88b `888P"Y88bP"Y88b 8 Y88. .P 888 888ooo88P'
* 888 `88b 888ooo888 .oP"888 888 888 888 8 `888' 888 888
* 888 .88P 888 .o d8( 888 888 888 888 8 Y 888 888
* o888bood8P' `Y8bod8P' `Y888""8o o888o o888o o888o o8o o888o o888o
* ========================================================================
* Updated: 2/11/22 22:17
* Copyright (c) 2019-2022 BeamMP Ltd. All rights reserved.
*/
require('dotenv').config()
const helmet = require("helmet");
var morgan = require('morgan');
const express = require('express')
const WebServer = express()
const routes = require('./routes')
const server = require('http').createServer(WebServer)
//////////////////////////////////////////////////////////////////////
// Cluster Handling
//////////////////////////////////////////////////////////////////////
const cluster = require('cluster');
if (!cluster.isMaster) {
cluster.worker.on('disconnect', function() {
console.log('Worker disconnected, closing server');
server.close(() => {
console.log('HTTP server closed')
})
});
}
//////////////////////////////////////////////////////////////////////
// Main Webserver
//////////////////////////////////////////////////////////////////////
;(app => {
'use strict'
module.exports.server = server
module.exports.app = app
module.exports.init = async (callback) => {
app.disable('x-powered-by')
if (process.env.DEBUG == "true") {
console.log('Request Logging Enabled.')
app.use(morgan('dev', {
skip: function (req, res) {
return res.statusCode == 200 || res.statusCode == 401
}
}))
}
app.use(helmet());
// CORS
app.use(allowCrossDomain)
// PROXY HANDLING
app.set('trust proxy', true);
// set the view engine to ejs
app.set('view engine', 'ejs');
app.use(express.static(require('path').join(__dirname, 'static')))
app.set("views", "./src/views")
routes(app)
if (typeof callback === 'function') callback()
}
module.exports.listen = (callback) => {
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
console.error('Address in use, exiting...')
server.close()
} else {
console.error(err.message)
throw err
}
})
server.listen(process.env.PORT, '0.0.0.0', () => {
console.log('Backend is now listening on port: ' + process.env.PORT)
if (typeof callback === 'function') return callback()
})
}
})(WebServer)
function allowCrossDomain (req, res, next) {
const allowedOrigins = ['http://127.0.0.1:3599', 'http://localhost:3599', 'https://beammp.com', 'https://backend.beammp.com', 'https://forum.beammp.com', 'https://docs.beammp.com'];
const origin = req.headers['origin'];
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
//res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000') // TODO: Update this to the keymaster domain
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
res.setHeader(
'Access-Control-Allow-Headers',
'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,authorization,X-RToken,X-Token,Origin'
)
res.setHeader('Content-Security-Policy', "frame-ancestors 'none';")
if (req.method === 'OPTIONS') {
res.sendStatus(200)
} else {
next()
}
}