Include policies, minor fixes and add anonymous analytics

This commit is contained in:
Starystars67
2026-06-16 16:43:43 +01:00
parent 74adf15820
commit 01281ebed2
22 changed files with 571 additions and 63 deletions
+2 -2
View File
@@ -198,7 +198,7 @@ const TikTokIcon = createLucideIcon('TikTok', [
</RouterLink>
<span>&middot;</span>
<a
href="https://forum.beammp.com/topic/95/privacy-policy-v1-0"
href="https://beammp.com/privacy"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-700 hover:text-beammp-blue transition-colors dark:text-neutral-500 dark:hover:text-blue-400"
@@ -207,7 +207,7 @@ const TikTokIcon = createLucideIcon('TikTok', [
</a>
<span>&middot;</span>
<a
href="https://forum.beammp.com/topic/94/terms-of-use-v1-0"
href="https://beammp.com/terms"
target="_blank"
rel="noopener noreferrer"
class="text-neutral-700 hover:text-beammp-blue transition-colors dark:text-neutral-500 dark:hover:text-blue-400"
+9 -9
View File
@@ -1,8 +1,8 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Check } from "lucide-vue-next";
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
import { reactiveOmit } from '@vueuse/core'
import { Check } from 'lucide-vue-next'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps({
defaultValue: { type: [Boolean, String], required: false },
@@ -15,12 +15,12 @@ const props = defineProps({
name: { type: String, required: false },
required: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
})
const emits = defineEmits(['update:modelValue'])
const delegatedProps = reactiveOmit(props, "class");
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
@@ -29,7 +29,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
:class="
cn(
'grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class,
props.class
)
"
>
+1 -1
View File
@@ -1 +1 @@
export { default as Checkbox } from "./Checkbox.vue";
export { default as Checkbox } from './Checkbox.vue'
+1 -1
View File
@@ -274,4 +274,4 @@
}
}
}
}
}
+7 -13
View File
@@ -133,33 +133,27 @@
"title": "Frequently Asked Questions",
"0": {
"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."
"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."
},
"1": {
"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."
"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."
},
"2": {
"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."
"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."
},
"3": {
"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."
"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."
},
"4": {
"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 docs."
"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 docs."
},
"5": {
"question": "Can I use mods?",
"answer":
"Mods are supported. These are installed on the server. See our docs for more information."
"answer": "Mods are supported. These are installed on the server. See our docs for more information."
}
},
"stats": {
@@ -225,7 +219,7 @@
"server_volume": "Server Volume Over Time",
"server_hint": "Server count follows similar trends. Release markers indicate major version launches."
},
"about":{
"about": {
"title": "About BeamMP",
"description": "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.",
"note": {
+1 -1
View File
@@ -274,4 +274,4 @@
}
}
}
}
}
+1 -1
View File
@@ -274,4 +274,4 @@
}
}
}
}
}
+27
View File
@@ -65,6 +65,27 @@ const baseRoutes = [
requiresAuth: false,
},
},
{
path: 'privacy',
name: 'Privacy',
component: () => import('@/views/Privacy.vue'),
meta: {
title: 'Privacy Policy - BeamMP',
description: 'Read our privacy policy',
requiresAuth: false,
},
},
{
path: 'terms',
name: 'Terms',
component: () => import('@/views/Terms.vue'),
meta: {
title: 'Terms of Service - BeamMP',
description: 'Read our terms of service',
requiresAuth: false,
},
},
]
const routes = [
@@ -106,6 +127,12 @@ const router = createRouter({
// Global navigation guard for meta data and locale
router.beforeEach(async (to, from, next) => {
// If the path has no locale prefix (e.g. /servers), redirect to /<lang>/servers
if (!to.params.locale && to.path !== '/') {
const locale = localStorage.getItem('lang') || 'en'
return next(`/${locale}${to.path}`)
}
const paramsLocale = to.params.locale || 'en'
const i18n = window.i18n
+7 -6
View File
@@ -1,17 +1,18 @@
@import "tailwindcss";
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@plugin "tailwindcss-animate";
@import "tw-animate-css";
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@theme {
--color-beammp-orange: #F36D24;
--color-beammp-white: #FFFFFF;
--color-beammp-orange: #f36d24;
--color-beammp-white: #ffffff;
--color-beammp-black: #000000;
--color-beammp-gray: #333333;
--color-beammp-green: #1D9749;
--color-beammp-blue: #4470B6;
--color-beammp-green: #1d9749;
--color-beammp-blue: #4470b6;
}
@theme inline {
+3 -3
View File
@@ -31,12 +31,12 @@ export function getLocalizedPath(route, locale = null) {
export function switchLocale(newLocale, currentPath) {
// Split path from query string
const [pathOnly] = currentPath.split('?')
// Remove locale from the beginning: /en/servers -> /servers, /en/ -> /
const pathWithoutLocale = pathOnly.replace(/^\/[a-z]{2}(?:\/|$)/, '/')
// Remove trailing slashes and ensure we have the right format
const cleanPath = pathWithoutLocale.replace(/\/+/g, '/').replace(/\/$/, '') || ''
return `/${newLocale}${cleanPath}`.replace(/\/+/g, '/')
}
+3 -1
View File
@@ -251,7 +251,9 @@ const communities = computed(() => [
<!-- 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">{{ $t('message.communities.starting.title') }}</h2>
<h2 class="text-3xl md:text-4xl font-bold">
{{ $t('message.communities.starting.title') }}
</h2>
<p class="text-lg text-neutral-600 dark:text-neutral-400">
{{ $t('message.communities.starting.description') }}
</p>
+5 -3
View File
@@ -30,7 +30,7 @@ const installerDownloadUrl = computed(() => {
return '/installer/BeamMP_Installer.exe'
}
return `${base.replace(/\/$/, '')}/installer/BeamMP_Installer.exe`
//return 'https://github.com/BeamMP/BeamMP-Launcher/releases/download/v2.8.0/BeamMP_Installer.exe'
})
@@ -231,7 +231,9 @@ const faqs = computed(() => [
{{ stat.value }} </span
>{{ stat.suffix }}
</div>
<div class="text-sm md:text-base text-neutral-700 dark:text-neutral-300 mt-1">{{ stat.label }}</div>
<div class="text-sm md:text-base text-neutral-700 dark:text-neutral-300 mt-1">
{{ stat.label }}
</div>
</div>
</div>
</div>
@@ -293,7 +295,7 @@ const faqs = computed(() => [
</div>
<div class="text-center mt-8">
<RouterLink
style="display: none;"
style="display: none"
:to="`/${$i18n.locale}/communities`"
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"
>
+16 -9
View File
@@ -4,25 +4,32 @@ import { RouterLink } from 'vue-router'
<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="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">{{ $t("message.404.title") }}</h1>
<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">{{ $t('message.404.title') }}</h1>
</div>
<p class="text-neutral-600 dark:text-neutral-300 max-w-prose mx-auto">
{{ $t("message.404.description") }}
{{ $t('message.404.description') }}
</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">
{{ $t("message.404.return_home") }}
class="inline-flex px-4 py-2 rounded-md bg-orange-600 hover:bg-orange-500 text-white text-sm font-medium"
>
{{ $t('message.404.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">
{{ $t("message.home.hero.browse_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"
>
{{ $t('message.home.hero.browse_servers') }}
</RouterLink>
</div>
</div>
+8 -5
View File
@@ -11,18 +11,18 @@ async function loadPartners() {
if (!res.ok) throw new Error(`Failed to load partners (${res.status})`)
const data = await res.json()
const partnersArray = Array.isArray(data) ? data : []
// Create a seed based on the current date (YYYY-MM-DD)
const today = new Date().toISOString().split('T')[0]
const seed = today.split('-').reduce((acc, val) => acc + parseInt(val), 0)
// Simple seeded shuffle using the date as seed
const shuffled = [...partnersArray]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(((seed * (i + 1) * 9301 + 49297) % 233280) / 233280 * (i + 1))
const j = Math.floor((((seed * (i + 1) * 9301 + 49297) % 233280) / 233280) * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
partners.value = shuffled
} catch (e) {
error.value = e.message || 'Unable to fetch partners'
@@ -70,7 +70,10 @@ onMounted(loadPartners)
:key="idx"
class="shadow-md hover:-translate-y-1 duration-150 group rounded-xl border border-neutral-200 dark:border-neutral-800 dark:bg-neutral-900/50 hover:border-beammp-orange dark:hover:border-beammp-orange hover:shadow-xl transition-all"
>
<div class="flex items-center justify-center min-h-16 mb-4 p-6 bg-neutral-900/80 border-b border-beammp-orange border-b-2 rounded-t-xl" style="height: 114px;">
<div
class="flex items-center justify-center min-h-16 mb-4 p-6 bg-neutral-900/80 border-b border-beammp-orange border-b-2 rounded-t-xl"
style="height: 114px"
>
<img :src="p.logo" :alt="p.name" class="max-h-16 object-contain" />
</div>
<div class="p-6">
+29
View File
@@ -0,0 +1,29 @@
<script setup>
import { ref, onMounted } from 'vue'
import { marked } from 'marked'
const content = ref('')
const isLoading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const res = await fetch('/docs/privacy_policy.md')
if (!res.ok) throw new Error('Failed to load privacy policy')
const text = await res.text()
content.value = marked.parse(text)
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
})
</script>
<template>
<section class="max-w-4xl mx-auto px-4 py-10">
<div v-if="isLoading" class="text-center py-20 text-muted-foreground">Loading...</div>
<div v-else-if="error" class="text-center py-20 text-destructive">{{ error }}</div>
<article v-else class="prose prose-neutral dark:prose-invert max-w-none" v-html="content" />
</section>
</template>
+6 -2
View File
@@ -2,7 +2,9 @@
<div class="servers-container">
<div class="servers-header">
<h1 class="text-4xl font-bold">{{ $t('message.servers.title') }}</h1>
<p class="text-base servers-subtitle text-neutral-600 dark:text-neutral-400">{{ $t('message.servers.description') }}</p>
<p class="text-base servers-subtitle text-neutral-600 dark:text-neutral-400">
{{ $t('message.servers.description') }}
</p>
</div>
<div class="servers-filters">
@@ -47,7 +49,9 @@
</div>
<div class="server-count">
{{ $t('message.servers.server_count', { count: filteredServers.length }) }} ({{ $t('message.servers.players_found', { count: totalPlayers }) }})
{{ $t('message.servers.server_count', { count: filteredServers.length }) }} ({{
$t('message.servers.players_found', { count: totalPlayers })
}})
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
<script setup>
import { ref, onMounted } from 'vue'
import { marked } from 'marked'
const content = ref('')
const isLoading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const res = await fetch('/docs/terms.md')
if (!res.ok) throw new Error('Failed to load terms of service')
const text = await res.text()
content.value = marked.parse(text)
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
})
</script>
<template>
<section class="max-w-4xl mx-auto px-4 py-10">
<div v-if="isLoading" class="text-center py-20 text-muted-foreground">Loading...</div>
<div v-else-if="error" class="text-center py-20 text-destructive">{{ error }}</div>
<article v-else class="prose prose-neutral dark:prose-invert max-w-none" v-html="content" />
</section>
</template>