with_posts #9

Open
Jukoga wants to merge 9 commits from with_posts into master
46 changed files with 1267 additions and 67 deletions

6
.gitignore vendored
View File

@ -164,3 +164,9 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# static
staticfiles/
# database
db.sqlite3

View File

@ -25,11 +25,9 @@ DEFAULT_AUTO_FIELD='django.db.models.AutoField'
# Load the Env
load_dotenv()
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'beyond-heroes.com', 'www.beyond-heroes.com']
@ -40,17 +38,25 @@ INSTALLED_APPS = [
'crispy_forms',
'crispy_bootstrap4',
'blog.apps.BlogConfig',
'users',
'api.apps.APIConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites', # for django-allauth
'corsheaders',
'dj_rest_auth',
'rest_framework',
'rest_framework.authtoken',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # cors-headers
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
@ -58,6 +64,8 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CORS_ORIGIN_WHITELIST = ( 'http://localhost:3000', 'http://www.beyond-heroes.com')
ROOT_URLCONF = 'BH.urls'
TEMPLATES = [
@ -112,6 +120,17 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.AllowAny',
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication', # causes CSRF Token conflicts in API
'rest_framework.authentication.TokenAuthentication',
)
}
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
@ -130,11 +149,15 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
# remember to `python manage.py collectstatic`
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR + '/media')
MEDIA_URL = '/media/'
CRISPY_TEMPLATE_PACK = 'bootstrap4'
LOGIN_REDIRECT_URL = 'News'
LOGIN_REDIRECT_URL = 'Home'
LOGIN_URL = 'Login'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # prints to console
SITE_ID = 1 # for django-allauth

View File

@ -1,4 +1,4 @@
"""TechBlog URL Configuration
"""BH URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
@ -21,6 +21,11 @@ from django.urls import path, include
urlpatterns = [
path('', include('blog.urls')),
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')),
path('users/', include('users.urls')),
path('api-auth/', include('rest_framework.urls')),
path('api/v1/dj-rest-auth/', include('dj_rest_auth.urls')),
# path('api/v1/dj-rest-auth/register/', include('dj_rest_auth.registration.urls')),
]
if settings.DEBUG:

View File

@ -30,5 +30,5 @@ EXPOSE 3030
# Define environment variable for Gunicorn
ENV GUNICORN_CMD_ARGS="--bind 0.0.0.0:3030"
# Run Gunicorn server with your Django application
CMD ["gunicorn", "BH.wsgi:application"]
# Collect static files and run Gunicorn
CMD ["sh", "-c", "python manage.py collectstatic --noinput && gunicorn BH.wsgi:application"]

0
api/__init__.py Normal file
View File

9
api/admin.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(Province)
admin.site.register(AssaultTroop)
admin.site.register(Player)
admin.site.register(Server)

5
api/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class APIConfig(AppConfig):
name = 'api'

8
api/filters.py Normal file
View File

@ -0,0 +1,8 @@
from rest_framework.filters import BaseFilterBackend
from users.models import *
class UserDataFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
queryset = queryset.filter(user__id=request.user.id)
return queryset

View File

@ -0,0 +1,71 @@
# Generated by Django 5.2.9 on 2026-02-19 10:09
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Player',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=50)),
('faction', models.IntegerField()),
('server', models.CharField(max_length=20)),
],
options={
'ordering': ['id'],
},
),
migrations.CreateModel(
name='Province',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('faction', models.IntegerField()),
('map', models.CharField(max_length=255)),
('mov_speed', models.IntegerField()),
('ats', models.JSONField(null=True)),
],
options={
'ordering': ['id'],
},
),
migrations.CreateModel(
name='Server',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False)),
('players', models.IntegerField()),
('capacity', models.IntegerField()),
('region', models.CharField(max_length=3)),
('address', models.CharField(max_length=20)),
],
options={
'ordering': ['id'],
},
),
migrations.CreateModel(
name='AssaultTroop',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('faction', models.IntegerField()),
('type', models.IntegerField()),
('province', models.IntegerField()),
('orders', models.JSONField(null=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ats', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['id'],
},
),
]

View File

70
api/models.py Normal file
View File

@ -0,0 +1,70 @@
from django.db import models
from django.contrib.auth.models import User
from django.db.models.fields import CharField, IntegerField
# Create your models here.
class Province(models.Model):
id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 100, blank = False)
faction = models.IntegerField() # 0 - Neutral, 1 - Allies, 2 - Axis
map = models.CharField(max_length=255)
mov_speed = models.IntegerField()
ats = models.JSONField(null=True)
class Meta:
ordering = ['id']
def __str__(self):
return f"{self.name} - {self.faction}"
class AssaultTroop(models.Model):
id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 100, blank = False)
faction = models.IntegerField() # 0 - Neutral, 1 - Allies, 2 - Axis
type = models.IntegerField()
province = models.IntegerField() # Province ID (-1 for not deployed)
orders = models.JSONField(null=True)
owner = models.ForeignKey(
'auth.User',
related_name='ats',
on_delete=models.CASCADE
)
class Meta:
ordering = ['id']
def __str__(self):
return f"{self.name} - {self.province},{self.faction}"
class Player(models.Model):
id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 50, blank = False)
faction = models.IntegerField()
server = CharField(max_length = 20, blank = False)
class Meta:
ordering = ['id']
def __str__(self):
return self.name
class Server(models.Model):
id = IntegerField(primary_key = True)
players = IntegerField() # total current players
capacity = IntegerField() # max player capacity
region = CharField(max_length = 3, blank = False) # 3 letter abb. for region
address = CharField(max_length = 20, blank = False)
class Meta:
ordering = ['id']
def __str__(self):
return f"{self.address} - {self.region}"

24
api/permissions.py Normal file
View File

@ -0,0 +1,24 @@
from rest_framework import permissions
class IsSuperUserOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_superuser
class IsStaff(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_staff
class IsSuperUserOrAuthReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return request.user != None
return request.user.is_superuser
class IsSuperUser(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_superuser

25
api/serializers.py Normal file
View File

@ -0,0 +1,25 @@
from rest_framework import serializers
from .models import *
class ProvinceSerializer(serializers.ModelSerializer):
class Meta:
model = Province
fields = ['id', 'name', 'faction', 'map', 'mov_speed', 'ats']
class AssaultTroopSerializer(serializers.ModelSerializer):
class Meta:
model = AssaultTroop
fields = ['id', 'name', 'faction', 'type', 'province', 'orders', 'owner']
class PlayerSerializer(serializers.ModelSerializer):
class Meta:
model = Player
fields = ['id', 'name', 'faction', 'server']
class ServerSerializer(serializers.ModelSerializer):
class Meta:
model = Server
fields = ['id', 'players', 'capacity', 'region', 'address']

View File

@ -0,0 +1,12 @@
from django import template
from django.template.defaultfilters import stringfilter
import markdown as md
register = template.Library()
@register.filter()
@stringfilter
def markdown(value):
return md.markdown(value, extensions=['markdown.extensions.fenced_code'])

3
api/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
api/urls.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import path
from .views import *
urlpatterns = [
path('provinces/', ProvincesView.as_view()),
path('provinces/<int:pk>/', ProvinceView.as_view()),
path('assault_troops/', AssaultTroopsView.as_view()),
path('assault_troops/<int:nm>/', AssaultTroopView.as_view()),
path('players/', PlayersView.as_view()),
path('players/<int:nm>/', PlayerView.as_view()),
path('servers/', ServersView.as_view()),
path('servers/<int:nm>/', ServerView.as_view()),
path('user_data/', UserDataView.as_view()),
path('user_data/<int:pk>/', UserDatumView.as_view()),
]

67
api/views.py Normal file
View File

@ -0,0 +1,67 @@
from rest_framework import generics, permissions
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from .models import *
from .filters import *
from .serializers import *
from .permissions import *
from users.models import *
from users.serializers import *
class ProvincesView(generics.ListCreateAPIView):
permission_classes = (IsSuperUserOrReadOnly,)
queryset = Province.objects.all()
serializer_class = ProvinceSerializer
class ProvinceView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsSuperUserOrReadOnly,)
queryset = Province.objects.all()
serializer_class = ProvinceSerializer
class AssaultTroopsView(generics.ListCreateAPIView):
permission_classes = (IsSuperUserOrReadOnly,)
queryset = AssaultTroop.objects.all()
serializer_class = AssaultTroopSerializer
class AssaultTroopView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsSuperUserOrReadOnly,)
queryset = AssaultTroop.objects.all()
serializer_class = AssaultTroopSerializer
class PlayersView(generics.ListCreateAPIView):
permission_classes = (IsStaff,) # Only Staff can see player info, i.e. authorized servers
queryset = Player.objects.all()
serializer_class = PlayerSerializer
class PlayerView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsStaff)
queryset = Player.objects.all()
serializer_class = PlayerSerializer
class ServersView(generics.ListCreateAPIView):
permission_classes = (IsSuperUserOrReadOnly,)
queryset = Server.objects.all()
serializer_class = ServerSerializer
class ServerView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsSuperUserOrReadOnly,)
queryset = Server.objects.all()
serializer_class = ServerSerializer
class UserDatumView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsSuperUser,)
queryset = UserData.objects.all()
serializer_class = UserDataSerializer
# filter_backends = [UserDataFilterBackend]
class UserDataView(generics.ListCreateAPIView):
permission_classes = (IsSuperUserOrAuthReadOnly,)
queryset = UserData.objects.all()
serializer_class = UserDataSerializer
filter_backends = [UserDataFilterBackend]

Binary file not shown.

5
server.bat Normal file
View File

@ -0,0 +1,5 @@
@ echo off
echo The devlopment server will start in a few seconds...
python manage.py makemigrations
python manage.py migrate
python manage.py runserver 3000

View File

@ -3,35 +3,536 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beyond Heroes</title>
<title>Beyond Heroes - Official Website</title>
<!-- <link rel="stylesheet" href="demo3.css"> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Mozilla+Headline" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
<style type="text/css">
:root {
--primary-color: #009020; /* Accents */
--primary-dark-color: #006020; /* Secondary Accents */
--secondary-color: #333; /* Dark gray for text */
--background-color: #1a1a1a; /* Dark background */
--card-bg-color: #2a2a2a; /* Background for cards */
/* --font-family: 'Montserrat', sans-serif;*/
}
@font-face {
font-family: 'Alte DIN 1451 Mittelschrift';
src: url('/media/mittelschrift.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
src: url('media/mittelschrift.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
}
body {
font-family: 'Alte DIN 1451 Mittelschrift', sans-serif;
}
/* Allgemeine Stile (General Styles) */
body {
margin: 0;
/* font-family: var(--font-family);*/
color: white;
background-color: var(--background-color);
line-height: 1.6;
}
h1, h2 {
font-family: 'Mozilla Headline';
letter-spacing: -1px;
margin: 0;
}
h3 {
margin: 0;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
section {
padding: 60px 20px;
}
hr {
border: 0;
border-top: 1px solid #444;
margin: 15px 0;
}
/* Hero-Bereich (Hero Section) */
.hero-section {
position: relative;
height: 100vh; /* Volle Bildschirmhöhe (Full viewport height) */
background-size: cover;
background-position: center;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
color: white;
}
.hero-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40px;
margin: 0px 20%;
border-radius: 10px;
}
.hero-section h1 {
font-size: 4.7em;
font-weight: bold;
margin-block-start: 0;
margin-block-end: 0;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
}
.hero-section p {
margin-block-start: 0;
font-size: 1.2em;
font-weight: lighter;
margin-bottom: 30px;
}
.discord-button {
background-color: var(--primary-color);
color: var(--background-color);
padding: 15px 30px;
letter-spacing: +0.8px;
border-radius: 50px;
font-weight: bold;
transition: background-color 0.3s ease;
}
.discord-button:hover {
background-color: var(--primary-dark-color);
text-decoration: none;
}
/* Team-Bereich (Team Section) */
.team-container {
display: flex;
gap: 40px;
max-width: 1200px;
margin: auto;
align-items: flex-start;
}
.dev-cards-column {
flex: 6;
display: flex;
flex-direction: column;
gap: 15px;
}
.dev-card {
display: flex;
align-items: center;
gap: 20px;
background-color: var(--card-bg-color);
padding: 15px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.dev-card:hover, .dev-card.active {
background-color: var(--primary-color);
color: var(--background-color);
}
.dev-card:hover .dev-info p, .dev-card.active .dev-info p {
color: var(--background-color);
}
.dev-card img {
border-radius: 50%;
width: 60px;
height: 60px;
object-fit: cover;
}
.dev-info h3 {
margin-block-end: 0;
}
.dev-info p {
margin-block-start: 0;
margin-block-end: 0;
font-style: italic;
color: #bbb;
}
.dev-bio-column {
flex: 10;
}
#dev-bio-box {
background-color: var(--card-bg-color);
padding: 30px;
border-radius: 8px;
min-height: 975px; /* Stellt sicher, dass die Box groß genug ist (Ensures the box is large enough) */
}
#dev-bio-box p {
margin-block-start: 5px;
margin-block-end: 0;
font-size: 1.2em;
}
#dev-bio-box h3 {
color: var(--primary-color);
margin-block-end: 0;
font-size: 1.5em;
}
/* NEUE NAV-BAR (NEW NAV BAR) */
#navbar {
position: absolute;
top: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 00px;
z-index: 1000;
transition: background-color 0.3s ease, padding 0.3s ease;
}
#navbar .nav-brand{
margin-left: 40px;
}
#navbar .nav-links-div{
margin-right: 40px;
}
#navbar.scrolled {
position: fixed;
padding: 20px 40px;
top: 0;
background-color: var(--background-color); /* Feste Hintergrundfarbe (Solid background color) */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
padding: 10px 40px;
}
#navbar.scrolled .nav-brand{
margin-left: 0px;
}
#navbar.scrolled .nav-links-div{
margin-right: 80px;
}
.nav-brand {
font-weight: bold;
font-size: 1.5em;
color: white;
}
.nav-links {
list-style: none;
letter-spacing: +0.6px;
display: flex;
gap: 30px;
margin: 0;
padding: 0;
}
.nav-links a {
color: white;
font-weight: bold;
transition: color 0.3s ease;
}
.nav-links button {
color: white;
font-weight: bold;
transition: color 0.3s ease;
background: transparent;
box-shadow: 0 0 0 #000;
border-width: 0;
font-size: 1em;
padding: 0px;
}
.nav-links a:hover, button:hover{
color: var(--primary-color);
text-decoration: none;
}
/* Responsive Design (Für kleinere Bildschirme) (For smaller screens) */
@media (max-width: 768px) {
.hero-section h1 {
font-size: 2.5em;
}
.hero-content p {
font-size: 1em;
}
.news-container, .team-container {
grid-template-columns: 1fr;
flex-direction: column;
}
.dev-bio-column {
min-height: auto;
}
}
/* NEUE ALLGEMEINE HILFSKLASSE (NEW GENERAL HELPER CLASS) */
.text-center {
text-align: center;
}
/* NEUER SUPPORT-BEREICH (NEW SUPPORT SECTION) */
#support-section {
/* margin-top: 90px;*/
padding: 80px 20px;
/* background-color: var(--card-bg-color);*/
}
#support-section h2 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 40px;
color: #eee;
}
#support-section h3 {
margin: 20px;
color: #ddd;
}
#support-section p {
max-width: 800px;
margin: auto;
font-size: 1.1em;
color: #bbb;
}
/* NEUER SUPPORT-BEREICH (NEW SUPPORT SECTION) */
#about-section {
padding: 80px 20px;
background-color: var(--card-bg-color);
}
#about-section h2 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 40px;
}
#about-section h3 {
margin: 20px;
}
#about-section p {
max-width: 800px;
margin: auto;
font-size: 1.1em;
color: #ccc;
}
/* Fußzeile (Footer) */
footer {
background-color: #111;
text-align: center;
padding: 30px;
margin-top: 50px;
font-size: 0.9em;
color: #bbb;
}
footer p {
margin: 0;
}
.footer-socials {
margin-bottom: 5px;
}
.footer-socials a {
color: #fff;
font-size: 2em; /* Größe der Icons (Size of the icons) */
margin: 0 50px;
transition: color 0.3s ease;
}
.footer-socials a:hover {
color: var(--primary-color);
}
/* Neuigkeiten-Bereich (News Section) */
#news-section h2, #team-section h2 {
text-align: center;
margin-bottom: 40px;
font-size: 2.5em;
color: var(--primary-color);
}
/* NEUE STILE FÜR DEN NEUIGKEITEN-BEREICH (NEW STYLES FOR NEWS SECTION) */
.news-container {
display: flex;
justify-content: center;
gap: 30px;
max-width: 1200px;
margin: auto;
flex-wrap: wrap; /* Erlaubt das Umbrechen auf kleineren Bildschirmen (Allows wrapping on smaller screens) */
}
.news-post {
background-color: var(--card-bg-color);
border-radius: 8px;
overflow: hidden; /* Stellt sicher, dass das Bild innerhalb der Form bleibt (Ensures the image stays within the shape) */
width: 350px; /* Feste Breite für jede Karte (Fixed width for each card) */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
transition: transform 0.3s ease;
}
.news-post:hover {
transform: translateY(-5px);
}
.news-post img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.post-content {
padding: 20px;
}
.news-post h3 {
color: var(--primary-color);
margin-block-end: 0;
font-size: 1.5em;
}
.news-post p {
font-size: 1.1em;
}
.news-post p.news-date {
margin-block-start: 0;
margin-block-end: 0;
font-style: italic;
font-size: 0.9em;
color: #bbb;
margin-bottom: 15px;
}
.news-post .read-more {
display: inline-block;
color: var(--primary-color);
font-weight: bold;
margin-top: 10px;
}
.news-post .read-more:hover {
text-decoration: underline;
}
/* ... Der Rest des CSS-Codes bleibt gleich ... */
/* Responsive Design (Für kleinere Bildschirme) */
@media (max-width: 768px) {
/* ... Die bereits existierenden Media-Query-Styles bleiben bestehen ... */
.news-container {
flex-direction: column;
align-items: center;
}
.news-post {
width: 90%; /* Nimmt fast die ganze Breite ein (Takes up almost the full width) */
max-width: 400px;
}
}
/*********************************************************************************************/
/* NEUER SUPPORT-BEREICH (NEW SUPPORT SECTION) */
#supporters-carousel {
padding-top: 50px;
text-align: center;
/* background-color: var(--card-bg-color);*/
}
#supporters-carousel h3 {
color: white;
font-size: 2em;
margin-bottom: 30px;
}
.carousel-container {
overflow: hidden;
white-space: nowrap;
position: relative;
max-width: 100%;
}
.carousel-content {
display: inline-block;
animation: scroll-left 20s linear infinite; /* Animation für das Karussell (Animation for the carousel) */
}
.supporter-item {
display: inline-block;
background-color: var(--card-bg-color);
border: 1px solid var(--primary-color);
color: white;
padding: 15px 25px;
border-radius: 8px;
margin: 0 10px;
text-align: center;
min-width: 150px;
}
.supporter-item h4 {
margin: 0;
font-size: 1.2em;
}
.supporter-item p {
margin: 5px 0 0;
font-size: 1.1em;
font-weight: bold;
color: var(--primary-color);
}
/* Animation Keyframes für das Scrollen (Animation keyframes for scrolling) */
@keyframes scroll-left {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #000;">
<a class="navbar-brand" href="/" style="font-size: 24px;margin-left: 20px;">
<img src="/media/Asset 3.png" alt="BH Logo" height="30" style="padding-right: 2px;"> Beyond Heroes
</a>
<div class="ml-auto"></div>
<div class="justify-content-center" style="margin-right: 20px;" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item" style="padding-right: 10px; padding-left: 10px;">
<a class="nav-link" href="/news">News</a>
<nav id="navbar">
<div class="nav-brand">Beyond Heroes</div>
<div class="nav-links-div">
<ul class="nav-links">
{% if user.is_authenticated %}
<li><a href="/#news-section">News</a></li>
{% else %}
<li><a href="/#hero-section">Home</a></li>
{% endif %}
<li><a href="/#about-section">About</a></li>
<li><a href="/dev/support">Support Us</a></li>
{% if user.is_authenticated %}
<li class="nav-item">
<form action="{% url 'Logout' %}" method="post">
{% csrf_token %}
<button class="nav-link" type="submit">Logout</button>
</form>
</li>
<li class="nav-item" style="padding-right: 10px; padding-left: 10px;">
<a class="nav-link" href="/dev/">Development</a>
</li>
<li class="nav-item" style="padding-right: 10px; padding-left: 10px;">
<a class="nav-link" href="/dev/support/">Support Us</a>
{% else %}
<!-- <li class="nav-item">
<a class="nav-link" href="{% url 'Login' %}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'Register' %}">Register</a>
</li> -->
{% endif %}
</ul>
</div>
</nav>
@ -40,14 +541,33 @@
{% endblock %}
<div style="padding: 30px;"></div>
<footer>
<div class="container">
<!-- <p style="text-align: center;">&copy; Beyond Heroes. All rights reserved.</p> -->
<div class="footer-socials">
<a href="https://www.youtube.com/@BeyondHeroesWWII/" target="blank" aria-label="YouTube"><i class="fab fa-youtube"></i></a>
<a href="https://discord.gg/gnnfKKuumg" target="blank" aria-label="Discord"><i class="fab fa-discord"></i></a>
<!-- <a href="#" aria-label="Twitter"><i class="fab fa-twitter"></i></a> -->
<a href="https://www.reddit.com/r/beyondheroes/" target="blank" aria-label="Reddit"><i class="fab fa-reddit"></i></a>
</div>
</footer>
<!-- Bootstrap JavaScript and dependencies -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script>
<script type="text/javascript">
const navbar = document.getElementById('navbar');
const heroSection = document.querySelector('.hero-section');
if (heroSection == null) {
navbar.classList.add('scrolled');
}
else {
window.addEventListener('scroll', () => {
const heroHeight = heroSection.offsetHeight;
if (window.scrollY > heroHeight - 70) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block content %}
{% load markdown_extras %}
<!-- Main Content -->
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<!-- Feed -->
<div class="card">
<div class="card-header">
Latest Posts
</div>
<div class="card-body">
{% for blog in blogs %}
<!-- Posts will be dynamically added here -->
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title"><a href="{% url 'Blog' blog.id %}" class="text-dark">{{ blog.title }}</a></h4>
<p class="card-text">{{ blog.content | markdown | safe }}</p>
</div>
</div>
<!-- End of Posts -->
{% endfor %}
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
Updates
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
{% for topic in topics %}
<li class="list-group-item">{{ topic }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -2,6 +2,7 @@
{% block content %}
{% load markdown_extras %}
<br><br>
<!-- Main Content -->
<div class="container mt-5">
<div class="row">

View File

@ -1,24 +1,62 @@
{% extends 'base.html' %}
{% block content %}
<style type="text/css">
body {
background-image: url('/media/bg.jpg'); background-size: cover; background-repeat: no-repeat;background-color: #000;
}
</style>
<div style="position: absolute; bottom: 1; right: 0;margin: 25px; width: 35%; background-color: rgba(0, 0, 0, 0.6); color: #eeeeee; border-radius: 2px;">
<div style="padding: 20px;">
<h2 style="text-align: justify;">Beyond Heroes</h2>
<p style="text-align: justify; font-size: 22px;">
The project that rose from the ashes of a game that was once. We aim to recreate the experience that we enjoyed throughout the years, while also adding new features that make it truly <i>Beyond</i> Heroes and Generals.<br>
Join Us on this journey to relive the old days and experience something new by clicking on the button below!
{% if not user.is_authenticated %}
<header class="hero-section" style="background-image: url('bg.png');">
<div class="hero-content">
<h1>BEYOND HEROES</h1>
<p style="letter-spacing: +0.7px;">The project that rose from the ashes of a game that brought us all together. We aim to recreate the experience that we enjoyed throughout the years, while also adding new features that make this game truly <i>Beyond Heroes</i> and Generals.</p>
<br>
<a href="{% url 'Register' %}" target="blank" class="discord-button">Join The Community</a>
<p style="padding-top: 20px; font-size: 1em">already a part? <a href="{% url 'Login' %}">Login</a></p>
</div>
</header>
<br>
{% else %}
<section style="margin-top: 60px;" id="news-section">
<h2>DEVELOPMENT NEWS</h2>
<div class="news-container">
<article class="news-post">
<img src="https://picsum.photos/seed/news-1/400/250" alt="Gameplay Video">
<div class="post-content">
<h3>New 3D Models!</h3>
<p class="news-date">20 April 2025</p>
<p>Look at the great looking 3D Models made by our skilled modellers... <a href="#news-section">Read More</a></p>
</div>
</article>
<article class="news-post">
<img src="https://picsum.photos/seed/news-4/400/250" alt="Alpha-Test">
<div class="post-content">
<h3>Happy New Year!</h3>
<p class="news-date">01 January 2025</p>
<p>This was a great year and we have made good progress... <a href="#news-section">Read More</a></p>
</div>
</article>
<article class="news-post">
<img src="https://picsum.photos/seed/news-3/400/250" alt="Community Umfrage">
<div class="post-content">
<h3>New 3D Models!</h3>
<p class="news-date">26 November 2024</p>
<p>Look at the great looking 3D Models made by our skilled modellers... <a href="#news-section">Read More</a></p>
</div>
</article>
</div>
</section>
{% endif %}
<br>
<section id="about-section" class="text-center">
<h2>ABOUT US</h2>
<h3 style="text-align: justify-all; letter-spacing: +0.5px;">Our Developers</h3>
<p style="text-align: justify;">
Made up of enthusiastic helpers from all over the globe, with a strong core in Germany, Poland, France, India, Argentina, Checkia, Slovakia, Canada, and more. Sharing ideas across so many cultures gives us fresh perspectives and helps us keep eyes on different parts that might be more relevant locally and their experience with other mmos. That way, we can reach farther and keep a foot in major countries around the globe. If you'd like to join, just fill out the <a href="https://forms.gle/wf4LTUHNW7hjPXcz6" target="blank">form</a> and don't be scared to hit us up on Discord! Our work environment is super hostile and we do require you to have a sense of humor and the latest version of sarcasm which you can get here <a href="https://en.wikipedia.org/wiki/Sarcasm" target="blank">Sarcasm-StopMakingNewBranches-win-x64.7z</a>
</p>
</div>
</div>
<div style="position: absolute; bottom: 0; right: 0; margin-right: 25px; margin-bottom: 30px;">
<a class="btn" href="https://discord.gg/gnnfKKuumg" style="padding: 10px 100px; font-family:'Trebuchet MS', sans-serif; font-size: 36px; font-weight: bold; background-color: #139358; color: white; border-radius: 2px;" onmouseover="this.style.backgroundColor='#0f7a4c'" onmouseout="this.style.backgroundColor='#139358'">
To Battle!
</a>
</div>
<h3 style="text-align: justify-all; letter-spacing: +0.5px;">Our Vision</h3>
<p style="text-align: justify;">
Beyond Heroes, aims to create an exciting World War gaming experience inspired by 'Heroes & Generals' but without the issues it had. Our goal is to provide the same immersive and challenging game while renewing some changes and upgrading the technical systems that preceded. Through strategic gameplay that made HnG so interesting, War, and balanced mechanics, our game aims to deliver a fun and rewarding experience that appeals to old and new players. We value our community and strive to provide a respectful and enjoyable environment that fosters teamwork, camaraderie, and sportsmanship. Also this text sounds corporate af, and thus this anti-climatic sentence is here to break with the same old corporate speech that makes us pay 5$ to open the game. The team is mostly comprised of people who share the same core values those being: Being against the new trend of p2w mechanics, combating <a href="https://colinmagazine.com/blogs/news/live-service-fatigue-are-we-tired-of-battle-passes-microtransactions" target="blank">microtransaction fatigue</a>, being in favour of <a href="https://www.stopkillinggames.com/" target="blank">stop killing games</a>, being in favour of <a href="https://www.youtube.com/watch?v=2_Dtmpe9qaQ" target="blank">clippy</a> and against anti-consumer practices <a href="https://www.forbes.com/sites/paultassi/2024/05/01/nba-2k-players-furious-about-kobe-bryant-collector-level-reward-removal/" target="blank">(1)</a> <a href="https://www.gamesindustry.biz/epic-ea-roblox-and-more-face-eu-complaint-over-tricking-players-into-spending" target="blank">(2)</a>, being absolutely against <a href="https://en.wikipedia.org/wiki/Enshittification" target="blank">enshittification</a>, and in favor of remakes.
</p>
</section>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<br><br>
<div class="container mt-5">
<form method="POST">
{% csrf_token %}
<div class="card mb-4">
<div class="card-header">
Login
</div>
<div class="card-body">
{{ form | crispy }}
<br>
<button class="btn btn-light" type="submit"> Login </button>
</div>
<div class="card-footer">
<small class="text-muted">
Don't Have An Account? <a class="link" href="{% url 'Register' %}">Register</a>
</small>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<br><br>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{message.tags}}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="container mt-5">
<div class="card mb-4">
<div class="card-header h5">
You have been Logged Out
</div>
<div class="card-body">
Hope you enjoyed Today!
</div>
<div class="card-footer">
<small class="text-muted">
Go to <a href="{% url 'Home' %}" class="link">Home</a> or <a href="{% url 'Login' %}" class="link" type="submit"> Log in </a> Again?
</small>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<br>
<br>
<div class="container mt-5">
<form method="POST">
{% csrf_token %}
<div class="card mb-4">
<div class="card-header">
Create an Account
</div>
<div class="card-body">
{{ form | crispy }}
<br>
<button class="btn btn-light sm" type="submit"> Register </button>
</div>
<div class="card-footer">
<small class="text-muted">
Already Have An Account? <a class="link" href="{% url 'Login' %}">Sign In</a>
</small>
</div>
</div>
</form>
</div>
{% endblock %}

0
users/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
users/admin.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from .models import UserData
@admin.register(UserData)
class UserDataAdmin(admin.ModelAdmin):
list_display = ('user', 'name', 'xp', 'money')
search_fields = ('user__username', 'name')
readonly_fields = ('user', 'xp', 'money', 'equipment', 'inventory')

33
users/forms.py Normal file
View File

@ -0,0 +1,33 @@
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
class UserRegisterForm(UserCreationForm):
email = forms.EmailField()
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email__iexact=email).exists():
raise forms.ValidationError('An account with this email address already exists.')
return email.lower()
class UserUpdateForm(forms.ModelForm):
email = forms.EmailField()
class Meta:
model = User
fields = ['username', 'email']
def clean_email(self):
email = self.cleaned_data.get('email')
# Allow the current user to keep their own email
if User.objects.filter(email__iexact=email).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError('An account with this email address already exists.')
return email.lower()

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2026-02-19 10:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 5.2.9 on 2026-02-28 03:45
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_profile_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserData',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=40)),
('xp', models.IntegerField()),
('money', models.IntegerField()),
('equipment', models.CharField(default='0;', max_length=1024)),
('inventory', models.CharField(default='0;', max_length=1024)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.DeleteModel(
name='Profile',
),
]

Binary file not shown.

22
users/models.py Normal file
View File

@ -0,0 +1,22 @@
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class UserData(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=40)
xp = models.IntegerField(default=0)
money = models.IntegerField(default=10000)
equipment = models.CharField(max_length=1024, default='0;', blank=True)
inventory = models.CharField(max_length=1024, default='0;', blank=True)
def __str__(self):
return f"{self.user.username}'s data"
# So this is my beutiful brainchild to keep user data in about 1KB per user, I'm not too sure but still
# it's worth the try, so the main payload is the inventory along the soliers an their weapons and vehicles
# along with the mods for everything which I have separated by `;`, `.` and `,` for soldier, equipment
# class and each equipment. I'll try a bit of bit-hacking to pack as much info of 20 bytes in 2 of the mods.
# ammo|--|sights|---|internals|---|, trigger|--|barrel|--|skins|----|, number|--------|

9
users/serializers.py Normal file
View File

@ -0,0 +1,9 @@
from rest_framework import serializers
from .models import UserData
class UserDataSerializer(serializers.ModelSerializer):
class Meta:
model = UserData
fields = ['user', 'name', 'xp', 'money', 'equipment', 'inventory']
read_only_fields = ['user', 'xp', 'money', 'equipment', 'inventory']

11
users/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path
from django.contrib.auth import views as login_view
from . import views
urlpatterns = [
# path('profile/', views.profile, name='Profile'),
# path('profile/<int:pk>', views.profile, name='NamedProfile'),
path('login/', login_view.LoginView.as_view(template_name='users/login.html'), name='Login'),
path('logout/', login_view.LogoutView.as_view(template_name='users/logout.html'), name='Logout'),
path('register/', views.register, name='Register')
]

33
users/views.py Normal file
View File

@ -0,0 +1,33 @@
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.views.generic import ListView
from .forms import UserRegisterForm
class UserListView(LoginRequiredMixin, ListView):
model = User
template_name = 'users/people.html'
context_object_name = 'users'
def explore(request):
return render(request, 'users/explore.html', {'title': 'Explore'})
def login(request):
return render(request, 'users/login.html', {'title': 'Login'})
def register(request):
if request.method == 'POST':
form = UserRegisterForm(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
messages.success(request, f'{username}! Your account has been created.')
return redirect('Login')
else:
form = UserRegisterForm()
return render(request, 'users/register.html', {'title': 'Register', 'form': form})