diff --git a/.gitignore b/.gitignore
index eca2a89b04a3e380907aa885e8b227e4611cec55..33687b8235f99519db5d1a665f0488a6b3274cea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ __pycache__/
 webpack-stats.json
 staticfiles
 node_modules
+shared/static/shared/*.{js,css,txt}
diff --git a/groups/admin.py b/groups/admin.py
index 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..2780a04ba241bd12fb5e4d74c2938614e6d26c3a 100644
--- a/groups/admin.py
+++ b/groups/admin.py
@@ -1,3 +1,12 @@
 from django.contrib import admin
 
-# Register your models here.
+from shared.admin import MarkdownxGuardedModelAdmin
+
+from .models import Group
+
+
+class GroupAdmin(MarkdownxGuardedModelAdmin):
+    autocomplete_fields = ("lectures",)
+
+
+admin.site.register(Group, GroupAdmin)
diff --git a/groups/apps.py b/groups/apps.py
index 86b49bc69390200e161abca3346269d60c54b2f0..8c383e046e915815d4edd30e17443dbef3541083 100644
--- a/groups/apps.py
+++ b/groups/apps.py
@@ -2,5 +2,6 @@ from django.apps import AppConfig
 
 
 class GroupsConfig(AppConfig):
-    default_auto_field = 'django.db.models.BigAutoField'
-    name = 'groups'
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "groups"
+    verbose_name = "Skupiny"
diff --git a/groups/models.py b/groups/models.py
index fbab9a6d5e8e818050b04cbb5410e1200dc8b29e..a18eb371e79373e798d973d68b9bd92217b17003 100644
--- a/groups/models.py
+++ b/groups/models.py
@@ -12,8 +12,7 @@ class Group(models.Model):
     lectures = models.ManyToManyField(
         "lectures.Lecture",
         blank=True,
-        null=True,
-        verbose_name="Události",
+        verbose_name="Lekce",
     )
 
     class Meta:
diff --git a/lectures/admin.py b/lectures/admin.py
index 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..07ab4440af7dd56c7d4d699d47d669d6d7eafb58 100644
--- a/lectures/admin.py
+++ b/lectures/admin.py
@@ -1,3 +1,64 @@
 from django.contrib import admin
 
+from shared.admin import MarkdownxGuardedModelAdmin
+
+from .models import (
+    Lecture,
+    LectureGroup,
+    LectureLector,
+    LectureMaterial,
+    LectureRecording,
+)
+
 # Register your models here.
+
+
+class IndexHiddenModelAdmin(MarkdownxGuardedModelAdmin):
+    def has_module_permission(self, request):
+        return False
+
+
+class LectureGroupAdmin(MarkdownxGuardedModelAdmin):
+    autocomplete_fields = ("user_groups",)
+    search_fields = ("name",)
+
+
+class LectureLectorInline(admin.TabularInline):
+    model = LectureLector
+    extra = 1
+
+
+class LectureRecordingInline(admin.TabularInline):
+    model = LectureRecording
+    extra = 1
+
+
+class LectureMaterialInline(admin.StackedInline):
+    model = LectureMaterial
+    extra = 1
+
+
+class LectureAdmin(MarkdownxGuardedModelAdmin):
+    inlines = (
+        LectureLectorInline,
+        LectureRecordingInline,
+        LectureMaterialInline,
+    )
+
+    autocomplete_fields = ("groups",)
+    search_fields = ("name", "description")
+    list_display = (
+        "name",
+        "timestamp",
+    )
+
+
+for model in (
+    LectureLector,
+    LectureMaterial,
+    LectureRecording,
+):
+    admin.site.register(model, IndexHiddenModelAdmin)
+
+admin.site.register(Lecture, LectureAdmin)
+admin.site.register(LectureGroup, LectureGroupAdmin)
diff --git a/lectures/apps.py b/lectures/apps.py
index 30d62f2847985f5046f071af0616c7d1202ac5cd..5518d56dc0e62c11c556ef9ef539c93c16cd98c2 100644
--- a/lectures/apps.py
+++ b/lectures/apps.py
@@ -2,5 +2,6 @@ from django.apps import AppConfig
 
 
 class LecturesConfig(AppConfig):
-    default_auto_field = 'django.db.models.BigAutoField'
-    name = 'lectures'
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "lectures"
+    verbose_name = "Lekce"
diff --git a/lectures/migrations/0001_initial.py b/lectures/migrations/0001_initial.py
index 549de6b5571b77807fd889b3f0a7027e4ec043b7..1728c286d902f81dc322fa078d7c5cf596f5e5f8 100644
--- a/lectures/migrations/0001_initial.py
+++ b/lectures/migrations/0001_initial.py
@@ -1,70 +1,167 @@
 # Generated by Django 4.1.4 on 2023-04-17 10:56
 
-from django.db import migrations, models
 import django.db.models.deletion
 import markdownx.models
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     initial = True
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name='Lecture',
+            name="Lecture",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Datum a čas konání')),
-                ('type', models.CharField(choices=[('recommended', 'Doporučené'), ('optional', 'Volitelné')], max_length=11, verbose_name='Typ')),
-                ('name', models.CharField(max_length=128, verbose_name='Název')),
-                ('description', markdownx.models.MarkdownxField(blank=True, help_text='Můžeš použít markdown.', null=True, verbose_name='Popis')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "timestamp",
+                    models.DateTimeField(
+                        blank=True, null=True, verbose_name="Datum a čas konání"
+                    ),
+                ),
+                (
+                    "type",
+                    models.CharField(
+                        choices=[
+                            ("recommended", "Doporučené"),
+                            ("optional", "Volitelné"),
+                        ],
+                        max_length=11,
+                        verbose_name="Typ",
+                    ),
+                ),
+                ("name", models.CharField(max_length=128, verbose_name="Název")),
+                (
+                    "description",
+                    markdownx.models.MarkdownxField(
+                        blank=True,
+                        help_text="Můžeš použít markdown.",
+                        null=True,
+                        verbose_name="Popis",
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Lekce',
-                'verbose_name_plural': 'Lekce',
+                "verbose_name": "Lekce",
+                "verbose_name_plural": "Lekce",
             },
         ),
         migrations.CreateModel(
-            name='Material',
+            name="Material",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128, verbose_name='Název')),
-                ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')),
-                ('file', models.FileField(blank=True, null=True, upload_to='', verbose_name='Soubor')),
-                ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='lectures.lecture', verbose_name='Událost')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=128, verbose_name="Název")),
+                (
+                    "link",
+                    models.URLField(
+                        blank=True, max_length=256, null=True, verbose_name="Odkaz"
+                    ),
+                ),
+                (
+                    "file",
+                    models.FileField(
+                        blank=True, null=True, upload_to="", verbose_name="Soubor"
+                    ),
+                ),
+                (
+                    "event",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="materials",
+                        to="lectures.lecture",
+                        verbose_name="Událost",
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Materiál',
-                'verbose_name_plural': 'Materiály',
+                "verbose_name": "Materiál",
+                "verbose_name_plural": "Materiály",
             },
         ),
         migrations.CreateModel(
-            name='LectureRecording',
+            name="LectureRecording",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128, verbose_name='Název')),
-                ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')),
-                ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='lectures.lecture', verbose_name='Událost')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=128, verbose_name="Název")),
+                (
+                    "link",
+                    models.URLField(
+                        blank=True, max_length=256, null=True, verbose_name="Odkaz"
+                    ),
+                ),
+                (
+                    "event",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="recordings",
+                        to="lectures.lecture",
+                        verbose_name="Událost",
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Nahrávka',
-                'verbose_name_plural': 'Nahrávky',
+                "verbose_name": "Nahrávka",
+                "verbose_name_plural": "Nahrávky",
             },
         ),
         migrations.CreateModel(
-            name='LectureLector',
+            name="LectureLector",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=128, verbose_name='Jméno')),
-                ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')),
-                ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lectors', to='lectures.lecture', verbose_name='Událost')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=128, verbose_name="Jméno")),
+                (
+                    "link",
+                    models.URLField(
+                        blank=True, max_length=256, null=True, verbose_name="Odkaz"
+                    ),
+                ),
+                (
+                    "event",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="lectors",
+                        to="lectures.lecture",
+                        verbose_name="Událost",
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Lektor',
-                'verbose_name_plural': 'Lektoři',
+                "verbose_name": "Lektor",
+                "verbose_name_plural": "Lektoři",
             },
         ),
     ]
diff --git a/lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py b/lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..39a23e3a523b6e537d839374260b37729410312a
--- /dev/null
+++ b/lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py
@@ -0,0 +1,41 @@
+# Generated by Django 4.1.4 on 2023-04-18 07:33
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LectureMaterial',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=128, verbose_name='Název')),
+                ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')),
+                ('file', models.FileField(blank=True, null=True, upload_to='', verbose_name='Soubor')),
+                ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='lectures.lecture', verbose_name='Událost')),
+            ],
+            options={
+                'verbose_name': 'Materiál',
+                'verbose_name_plural': 'Materiály',
+            },
+        ),
+        migrations.RenameField(
+            model_name='lecturelector',
+            old_name='event',
+            new_name='lecture',
+        ),
+        migrations.RenameField(
+            model_name='lecturerecording',
+            old_name='event',
+            new_name='lecture',
+        ),
+        migrations.DeleteModel(
+            name='Material',
+        ),
+    ]
diff --git a/lectures/migrations/0003_alter_lecturematerial_file_and_more.py b/lectures/migrations/0003_alter_lecturematerial_file_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..b605f4d22065fc1135e18a35d91ca1e8e8bd7c00
--- /dev/null
+++ b/lectures/migrations/0003_alter_lecturematerial_file_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.4 on 2023-04-18 07:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0002_lecturematerial_rename_event_lecturelector_lecture_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='lecturematerial',
+            name='file',
+            field=models.FileField(blank=True, help_text='Zadej prosím pouze odkaz, nebo soubor.', null=True, upload_to='', verbose_name='Soubor'),
+        ),
+        migrations.AlterField(
+            model_name='lecturematerial',
+            name='link',
+            field=models.URLField(blank=True, help_text='Zadej prosím pouze odkaz, nebo soubor.', max_length=256, null=True, verbose_name='Odkaz'),
+        ),
+    ]
diff --git a/lectures/migrations/0004_alter_lecturematerial_file_and_more.py b/lectures/migrations/0004_alter_lecturematerial_file_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..a44a7da2335db04ebf6af88419a1037e406a3ed5
--- /dev/null
+++ b/lectures/migrations/0004_alter_lecturematerial_file_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.1.4 on 2023-04-18 07:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0003_alter_lecturematerial_file_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='lecturematerial',
+            name='file',
+            field=models.FileField(blank=True, help_text='Pokud máš vložený soubor, nemůžeš definovat odkaz.', null=True, upload_to='', verbose_name='Soubor'),
+        ),
+        migrations.AlterField(
+            model_name='lecturematerial',
+            name='link',
+            field=models.URLField(blank=True, help_text='Pokud máš zadaný odkaz, nemůžeš definovat soubor.', max_length=256, null=True, verbose_name='Odkaz'),
+        ),
+    ]
diff --git a/lectures/migrations/0005_lecturegroup.py b/lectures/migrations/0005_lecturegroup.py
new file mode 100644
index 0000000000000000000000000000000000000000..0ac154f3704676bb74a69c0a7b9efca67522a895
--- /dev/null
+++ b/lectures/migrations/0005_lecturegroup.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.1.4 on 2023-04-18 09:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth', '0012_alter_user_first_name_max_length'),
+        ('lectures', '0004_alter_lecturematerial_file_and_more'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LectureGroup',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=128, verbose_name='Jméno')),
+                ('lectures', models.ManyToManyField(blank=True, related_name='groups', to='lectures.lecture', verbose_name='Lekce')),
+                ('user_groups', models.ManyToManyField(to='auth.group', verbose_name='Uživatelské skupiny')),
+            ],
+            options={
+                'verbose_name': 'Skupina',
+                'verbose_name_plural': 'Skupiny',
+            },
+        ),
+    ]
diff --git a/lectures/migrations/0006_alter_lecturegroup_options_and_more.py b/lectures/migrations/0006_alter_lecturegroup_options_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..498885384ed5df7c42d5ccca1f2ce5aaea78f7fe
--- /dev/null
+++ b/lectures/migrations/0006_alter_lecturegroup_options_and_more.py
@@ -0,0 +1,39 @@
+# Generated by Django 4.1.4 on 2023-04-18 10:12
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth', '0012_alter_user_first_name_max_length'),
+        ('lectures', '0005_lecturegroup'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='lecturegroup',
+            options={'verbose_name': 'Výuková skupina', 'verbose_name_plural': 'Výukové skupiny'},
+        ),
+        migrations.AlterField(
+            model_name='lecturegroup',
+            name='user_groups',
+            field=models.ManyToManyField(blank=True, help_text='Pokud nedefinuješ žádné, lekce ve skupině jsou dostupné všem.', to='auth.group', verbose_name='Uživatelské skupiny'),
+        ),
+        migrations.AlterField(
+            model_name='lecturelector',
+            name='lecture',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lectors', to='lectures.lecture', verbose_name='Lekce'),
+        ),
+        migrations.AlterField(
+            model_name='lecturematerial',
+            name='lecture',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='lectures.lecture', verbose_name='Lekce'),
+        ),
+        migrations.AlterField(
+            model_name='lecturerecording',
+            name='lecture',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='lectures.lecture', verbose_name='Lekce'),
+        ),
+    ]
diff --git a/lectures/migrations/0007_alter_lecture_options.py b/lectures/migrations/0007_alter_lecture_options.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1dbc97bc04fa756d92dfa16ea1d921bdafe5519
--- /dev/null
+++ b/lectures/migrations/0007_alter_lecture_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.4 on 2023-04-18 10:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0006_alter_lecturegroup_options_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='lecture',
+            options={'ordering': ('-timestamp',), 'verbose_name': 'Lekce', 'verbose_name_plural': 'Lekce'},
+        ),
+    ]
diff --git a/lectures/migrations/0008_alter_lecture_options.py b/lectures/migrations/0008_alter_lecture_options.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa169f9602532418b53309a8d50b724d677c6db3
--- /dev/null
+++ b/lectures/migrations/0008_alter_lecture_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.4 on 2023-04-18 10:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0007_alter_lecture_options'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='lecture',
+            options={'ordering': ('-timestamp', '-name'), 'verbose_name': 'Lekce', 'verbose_name_plural': 'Lekce'},
+        ),
+    ]
diff --git a/lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py b/lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cc5167c10e1548266b5188590873e90b2b52cda
--- /dev/null
+++ b/lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.1.4 on 2023-04-18 10:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0008_alter_lecture_options'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='lecturegroup',
+            name='lectures',
+        ),
+        migrations.AddField(
+            model_name='lecture',
+            name='groups',
+            field=models.ManyToManyField(blank=True, related_name='lectures', to='lectures.lecturegroup', verbose_name='Výukové skupiny'),
+        ),
+    ]
diff --git a/lectures/migrations/0010_alter_lecturegroup_options.py b/lectures/migrations/0010_alter_lecturegroup_options.py
new file mode 100644
index 0000000000000000000000000000000000000000..339cb6c7a1afd75b287cddc44533760af2e8de8e
--- /dev/null
+++ b/lectures/migrations/0010_alter_lecturegroup_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.4 on 2023-04-18 10:29
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0009_remove_lecturegroup_lectures_lecture_groups'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='lecturegroup',
+            options={'ordering': ('name',), 'verbose_name': 'Výuková skupina', 'verbose_name_plural': 'Výukové skupiny'},
+        ),
+    ]
diff --git a/lectures/models.py b/lectures/models.py
index 81cd2a0163c90d9003b3dbf75ae51760fccff4e3..cd8ea810d5c17886005ab4563b2f1881bf35c022 100644
--- a/lectures/models.py
+++ b/lectures/models.py
@@ -1,14 +1,38 @@
 from datetime import timedelta
 
+from django.contrib.auth.models import Group
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.utils import timezone
 from markdownx.models import MarkdownxField
 
+from shared.models import NameStrMixin
+
 # Create your models here.
 
 
-class Lecture(models.Model):
+class LectureGroup(NameStrMixin, models.Model):
+    name = models.CharField(
+        max_length=128,
+        verbose_name="Jméno",
+    )
+
+    user_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        verbose_name="Uživatelské skupiny",
+        help_text=("Pokud nedefinuješ žádné, lekce ve skupině jsou dostupné všem."),
+    )
+
+    class Meta:
+        verbose_name = "Výuková skupina"
+        verbose_name_plural = "Výukové skupiny"
+        ordering = ("name",)
+
+
+class Lecture(NameStrMixin, models.Model):
+    is_current_treshold = timedelta(hours=8)
+
     timestamp = models.DateTimeField(
         verbose_name="Datum a čas konání",
         blank=True,
@@ -19,6 +43,13 @@ class Lecture(models.Model):
         RECOMMENDED = "recommended", "Doporučené"
         OPTIONAL = "optional", "Volitelné"
 
+    groups = models.ManyToManyField(
+        LectureGroup,
+        blank=True,
+        related_name="lectures",
+        verbose_name="Výukové skupiny",
+    )
+
     type = models.CharField(
         choices=TypeChoices.choices,
         max_length=11,
@@ -40,19 +71,20 @@ class Lecture(models.Model):
     @property
     def is_current(self) -> bool:
         # On or after the current time, minus 8 hours
-        return self.timestamp >= (timezone.now() - timedelta(hours=8))
+        return self.timestamp >= (timezone.now() - self.is_current_treshold)
 
     class Meta:
         verbose_name = "Lekce"
         verbose_name_plural = verbose_name
+        ordering = ("-timestamp", "-name")
 
 
-class LectureLector(models.Model):
-    event = models.ForeignKey(
+class LectureLector(NameStrMixin, models.Model):
+    lecture = models.ForeignKey(
         "Lecture",
         on_delete=models.CASCADE,
         related_name="lectors",
-        verbose_name="Událost",
+        verbose_name="Lekce",
     )
 
     name = models.CharField(
@@ -72,12 +104,12 @@ class LectureLector(models.Model):
         verbose_name_plural = "Lektoři"
 
 
-class LectureRecording(models.Model):
-    event = models.ForeignKey(
+class LectureRecording(NameStrMixin, models.Model):
+    lecture = models.ForeignKey(
         "Lecture",
         on_delete=models.CASCADE,
         related_name="recordings",
-        verbose_name="Událost",
+        verbose_name="Lekce",
     )
 
     name = models.CharField(
@@ -97,12 +129,12 @@ class LectureRecording(models.Model):
         verbose_name_plural = "Nahrávky"
 
 
-class Material(models.Model):
-    event = models.ForeignKey(
+class LectureMaterial(NameStrMixin, models.Model):
+    lecture = models.ForeignKey(
         "Lecture",
         on_delete=models.CASCADE,
         related_name="materials",
-        verbose_name="Událost",
+        verbose_name="Lekce",
     )
 
     name = models.CharField(
@@ -115,25 +147,28 @@ class Material(models.Model):
         blank=True,
         null=True,
         verbose_name="Odkaz",
+        help_text="Pokud máš zadaný odkaz, nemůžeš definovat soubor.",
     )
 
     file = models.FileField(
         blank=True,
         null=True,
         verbose_name="Soubor",
+        help_text="Pokud máš vložený soubor, nemůžeš definovat odkaz.",
     )
 
     def clean(self) -> None:
         BOTH_FILE_AND_LINK_DEFINED_ERROR = (
-            "Definuj prosím pouze odkaz, nebo soubor. "
-            "Nemůžeš mít oboje najednou."
+            "Definuj prosím pouze odkaz, nebo soubor. Nemůžeš mít oboje najednou."
         )
 
         if self.file and self.link:
-            raise ValidationError({
-                "link": BOTH_FILE_AND_LINK_DEFINED_ERROR,
-                "file": BOTH_FILE_AND_LINK_DEFINED_ERROR,
-            })
+            raise ValidationError(
+                {
+                    "link": BOTH_FILE_AND_LINK_DEFINED_ERROR,
+                    "file": BOTH_FILE_AND_LINK_DEFINED_ERROR,
+                }
+            )
 
     class Meta:
         verbose_name = "Materiál"
diff --git a/lectures/templates/lectures/index.html b/lectures/templates/lectures/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..c2f99e340942f8194f69d7b444492a47070f0dba
--- /dev/null
+++ b/lectures/templates/lectures/index.html
@@ -0,0 +1,6 @@
+{% extends "shared/includes/base.html" %}
+{% load markdownify %}
+
+{% block content %}
+    a
+{% endblock %}
diff --git a/lectures/urls.py b/lectures/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..004bc535cdee9c938950982e1225a54e937f0e83
--- /dev/null
+++ b/lectures/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from . import models, views
+
+app_name = "lectures"
+urlpatterns = [
+    path("", views.view_avilable_groups, name="view_avilable_groups"),
+]
diff --git a/lectures/views.py b/lectures/views.py
index 91ea44a218fbd2f408430959283f0419c921093e..fe8c0d093b6060cc1cc0b1ee0efef281ddfb347d 100644
--- a/lectures/views.py
+++ b/lectures/views.py
@@ -1,3 +1,51 @@
-from django.shortcuts import render
+from django.db import models
+from django.shortcuts import get_object_or_404, render
+from guardian.shortcuts import get_objects_for_user
 
-# Create your views here.
+from .models import LectureGroup
+
+
+def view_avilable_groups(request):
+    lecture_groups = (
+        get_objects_for_user(request.user, "lectures.view_lecturegroup")
+        .filter(user_groups__in=request.user.groups.all())
+        .distinct()
+        .all()
+    )
+
+    return render(
+        request,
+        "lectures/view_groups.html",
+        {
+            "title": "Výukové skupiny",
+            "site_url": "https://ucebnice.pirati.cz",  # TODO
+            "description": "Kurzy a školení zaměřené na politickou práci a organizaci kampaní.",
+            "header_name": "Pirátský e-Learning",
+            "lecture_groups": lecture_groups,
+        },
+    )
+
+
+def view_lectures(request, group_id: int):
+    group = get_object_or_404(
+        get_objects_for_user(request.user, "lectures.view_lecturegroup"),
+        id=group_id,
+    )
+
+    lectures = (
+        get_objects_for_user(request.user, "lectures.view_lecture")
+        .filter(groups=group)
+        .all()
+    )
+
+    return render(
+        request,
+        "lectures/view_lectures.html",
+        {
+            "title": f"Výuka pro {group.name}",
+            "site_url": "https://ucebnice.pirati.cz",  # TODO
+            "description": f"e-Learningová výuka pro skupinu {group.name}.",
+            "header_name": group.name,
+            "lectures": lectures,
+        },
+    )
diff --git a/manage.py b/manage.py
index b792cc0ecafa20e96f50afbd350bc29719c51800..701b8e3665a69f161e23733263eaccb07b868d13 100755
--- a/manage.py
+++ b/manage.py
@@ -6,7 +6,7 @@ import sys
 
 def main():
     """Run administrative tasks."""
-    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ucebnice.settings')
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucebnice.settings")
     try:
         from django.core.management import execute_from_command_line
     except ImportError as exc:
@@ -18,5 +18,5 @@ def main():
     execute_from_command_line(sys.argv)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif b/media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif
new file mode 100644
index 0000000000000000000000000000000000000000..676aff60c51e1b4301828c99f241174cf3b7ab71
Binary files /dev/null and b/media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif differ
diff --git a/oidc/apps.py b/oidc/apps.py
index 742acac1713eae78fd9b35b887dfc5dfe22b1989..eab6c95cfb1797f32823a2978cd8d40d63d52c42 100644
--- a/oidc/apps.py
+++ b/oidc/apps.py
@@ -4,3 +4,4 @@ from django.apps import AppConfig
 class OidcConfig(AppConfig):
     default_auto_field = "django.db.models.BigAutoField"
     name = "oidc"
+    verbose_name = "OpenID Přihlašování"
diff --git a/oidc/auth.py b/oidc/auth.py
index 0a0e359ec6e997b85d5fb3eb92d734f505a1af8b..97ef6a2a8c0cedbbe9a4fd710bc1a97866e71370 100644
--- a/oidc/auth.py
+++ b/oidc/auth.py
@@ -53,6 +53,8 @@ class UcebniceOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend):
             access_token, options={"verify_signature": False}
         )
 
+        user.sso_username = decoded_access_token["preferred_username"]
+        user.email = decoded_access_token["email"]
         user_groups = user.groups.all()
 
         self._remove_old_user_groups(
diff --git a/shared/static/shared/.gitkeep b/shared/static/shared/.gitkeep
deleted file mode 100644
index 8d1c8b69c3fce7bea45c73efd06983e3c419a92f..0000000000000000000000000000000000000000
--- a/shared/static/shared/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
- 
diff --git a/shared/static/shared/base.js b/shared/static/shared/base.js
index d52da305ca4c673c848f91497ca84cbb9df9970f..219ecfeeed80d9a217ff743e4f5edbc80bfe36e4 100644
--- a/shared/static/shared/base.js
+++ b/shared/static/shared/base.js
@@ -1 +1 @@
-(self.webpackChunkucebnice=self.webpackChunkucebnice||[]).push([[348],{208:()=>{}},e=>{e(e.s=208)}]);
\ No newline at end of file
+(self.webpackChunkucebnice=self.webpackChunkucebnice||[]).push([[348],{208:()=>{}},e=>{e(e.s=208)}]);
diff --git a/shared/static/shared/favicon.png b/shared/static/shared/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..819126c749b3335134893d13561fd74a007e8f2a
Binary files /dev/null and b/shared/static/shared/favicon.png differ
diff --git a/shared/static/shared/runtime.js b/shared/static/shared/runtime.js
index 52a82802fa3960a8c24b6bf124d26ab5b5de92dc..3babfc0573770a7488e1255395385c8cf11c360b 100644
--- a/shared/static/shared/runtime.js
+++ b/shared/static/shared/runtime.js
@@ -1 +1 @@
-(()=>{"use strict";var r,e={},n={};function o(r){var t=n[r];if(void 0!==t)return t.exports;var i=n[r]={exports:{}};return e[r](i,i.exports,o),i.exports}o.m=e,r=[],o.O=(e,n,t,i)=>{if(!n){var a=1/0;for(l=0;l<r.length;l++){for(var[n,t,i]=r[l],s=!0,c=0;c<n.length;c++)(!1&i||a>=i)&&Object.keys(o.O).every((r=>o.O[r](n[c])))?n.splice(c--,1):(s=!1,i<a&&(a=i));if(s){r.splice(l--,1);var f=t();void 0!==f&&(e=f)}}return e}i=i||0;for(var l=r.length;l>0&&r[l-1][2]>i;l--)r[l]=r[l-1];r[l]=[n,t,i]},o.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={666:0};o.O.j=e=>0===r[e];var e=(e,n)=>{var t,i,[a,s,c]=n,f=0;if(a.some((e=>0!==r[e]))){for(t in s)o.o(s,t)&&(o.m[t]=s[t]);if(c)var l=c(o)}for(e&&e(n);f<a.length;f++)i=a[f],o.o(r,i)&&r[i]&&r[i][0](),r[i]=0;return o.O(l)},n=self.webpackChunkucebnice=self.webpackChunkucebnice||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))})()})();
\ No newline at end of file
+(()=>{"use strict";var r,e={},n={};function o(r){var t=n[r];if(void 0!==t)return t.exports;var i=n[r]={exports:{}};return e[r](i,i.exports,o),i.exports}o.m=e,r=[],o.O=(e,n,t,i)=>{if(!n){var a=1/0;for(l=0;l<r.length;l++){for(var[n,t,i]=r[l],s=!0,c=0;c<n.length;c++)(!1&i||a>=i)&&Object.keys(o.O).every((r=>o.O[r](n[c])))?n.splice(c--,1):(s=!1,i<a&&(a=i));if(s){r.splice(l--,1);var f=t();void 0!==f&&(e=f)}}return e}i=i||0;for(var l=r.length;l>0&&r[l-1][2]>i;l--)r[l]=r[l-1];r[l]=[n,t,i]},o.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={666:0};o.O.j=e=>0===r[e];var e=(e,n)=>{var t,i,[a,s,c]=n,f=0;if(a.some((e=>0!==r[e]))){for(t in s)o.o(s,t)&&(o.m[t]=s[t]);if(c)var l=c(o)}for(e&&e(n);f<a.length;f++)i=a[f],o.o(r,i)&&r[i]&&r[i][0](),r[i]=0;return o.O(l)},n=self.webpackChunkucebnice=self.webpackChunkucebnice||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))})()})();
diff --git a/shared/static/shared/style.css b/shared/static/shared/style.css
index 7958c7a8505e637ccdcbcb34981615e160c303a0..d11e2fb9672f2cc076b54a7ad91f5139f3366618 100644
--- a/shared/static/shared/style.css
+++ b/shared/static/shared/style.css
@@ -520,18 +520,173 @@ html {
   --tw-backdrop-sepia:  ;
 }
 
+.container {
+  width: 100%;
+}
+
+@media (min-width: 640px) {
+  .container {
+    max-width: 640px;
+  }
+}
+
+@media (min-width: 768px) {
+  .container {
+    max-width: 768px;
+  }
+}
+
+@media (min-width: 1024px) {
+  .container {
+    max-width: 1024px;
+  }
+}
+
+@media (min-width: 1280px) {
+  .container {
+    max-width: 1280px;
+  }
+}
+
+@media (min-width: 1536px) {
+  .container {
+    max-width: 1536px;
+  }
+}
+
 .static {
   position: static;
 }
 
+.my-4 {
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+}
+
+.mb-4 {
+  margin-bottom: 1rem;
+}
+
+.mb-6 {
+  margin-bottom: 1.5rem;
+}
+
+.ml-8 {
+  margin-left: 2rem;
+}
+
 .block {
   display: block;
 }
 
+.inline-block {
+  display: inline-block;
+}
+
 .flex {
   display: flex;
 }
 
+.w-32 {
+  width: 8rem;
+}
+
+.w-8 {
+  width: 2rem;
+}
+
+.cursor-pointer {
+  cursor: pointer;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.gap-2 {
+  gap: 0.5rem;
+}
+
+.space-x-2 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-x-reverse: 0;
+  margin-right: calc(0.5rem * var(--tw-space-x-reverse));
+  margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
+.space-x-4 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-x-reverse: 0;
+  margin-right: calc(1rem * var(--tw-space-x-reverse));
+  margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
+.space-y-2 > :not([hidden]) ~ :not([hidden]) {
+  --tw-space-y-reverse: 0;
+  margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
+  margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
+}
+
+.self-start {
+  align-self: flex-start;
+}
+
+.border-r {
+  border-right-width: 1px;
+}
+
+.py-4 {
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+
+.py-8 {
+  padding-top: 2rem;
+  padding-bottom: 2rem;
+}
+
+.pb-4 {
+  padding-bottom: 1rem;
+}
+
+.pb-6 {
+  padding-bottom: 1.5rem;
+}
+
+.pl-4 {
+  padding-left: 1rem;
+}
+
+.pr-8 {
+  padding-right: 2rem;
+}
+
+.text-lg {
+  font-size: 1.125rem;
+  line-height: 1.75rem;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+  line-height: 1.25rem;
+}
+
+.text-xl {
+  font-size: 1.25rem;
+  line-height: 1.75rem;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+
+.text-white {
+  --tw-text-opacity: 1;
+  color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
 .transition {
   transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
   transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@@ -539,3 +694,111 @@ html {
   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
   transition-duration: 150ms;
 }
+
+.hover\:text-white:hover {
+  --tw-text-opacity: 1;
+  color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
+@media (min-width: 640px) {
+  .sm\:flex-row {
+    flex-direction: row;
+  }
+
+  .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-x-reverse: 0;
+    margin-right: calc(1rem * var(--tw-space-x-reverse));
+    margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
+  }
+
+  .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
+    margin-bottom: calc(0px * var(--tw-space-y-reverse));
+  }
+}
+
+@media (min-width: 768px) {
+  .md\:w-40 {
+    width: 10rem;
+  }
+
+  .md\:flex-row {
+    flex-direction: row;
+  }
+
+  .md\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--tw-space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
+  }
+
+  .md\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
+    margin-bottom: calc(0px * var(--tw-space-y-reverse));
+  }
+}
+
+@media (min-width: 1024px) {
+  .lg\:my-0 {
+    margin-top: 0px;
+    margin-bottom: 0px;
+  }
+
+  .lg\:mb-0 {
+    margin-bottom: 0px;
+  }
+
+  .lg\:flex-col {
+    flex-direction: column;
+  }
+
+  .lg\:items-end {
+    align-items: flex-end;
+  }
+
+  .lg\:space-x-0 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-x-reverse: 0;
+    margin-right: calc(0px * var(--tw-space-x-reverse));
+    margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
+  }
+
+  .lg\:space-y-2 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-y-reverse: 0;
+    margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
+    margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
+  }
+
+  .lg\:py-16 {
+    padding-top: 4rem;
+    padding-bottom: 4rem;
+  }
+
+  .lg\:py-24 {
+    padding-top: 6rem;
+    padding-bottom: 6rem;
+  }
+
+  .lg\:text-right {
+    text-align: right;
+  }
+}
+
+@media (min-width: 1280px) {
+  .xl\:flex-row {
+    flex-direction: row;
+  }
+
+  .xl\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-x-reverse: 0;
+    margin-right: calc(0.5rem * var(--tw-space-x-reverse));
+    margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
+  }
+
+  .xl\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
+    --tw-space-y-reverse: 0;
+    margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
+    margin-bottom: calc(0px * var(--tw-space-y-reverse));
+  }
+}
diff --git a/shared/templates/shared/includes/base.html b/shared/templates/shared/includes/base.html
new file mode 100644
index 0000000000000000000000000000000000000000..8f3808cc4ad86b1249ade378e9af0889cc48893e
--- /dev/null
+++ b/shared/templates/shared/includes/base.html
@@ -0,0 +1,198 @@
+{% load static %}
+{% load render_bundle from webpack_loader %}
+
+<!DOCTYPE html>
+<html lang="cs">
+    <head>
+        <meta charset="utf-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+
+        <meta name="title" content="{{ title }} | Učebnice">
+        <meta name="description" content="{{ description }}">
+
+        {% comment %}Open Graph / Facebook{% endcomment %}
+        <meta property="og:type" content="website">
+        <meta property="og:url" content="{{ site_url }}">
+        <meta property="og:title" content="{{ title }} | Učebnice">
+        <meta property="og:description" content="{{ description }}">
+        {% comment %}<meta property="og:image" content="">{% endcomment %}
+
+        {% comment %}Twitter{% endcomment %}
+        <meta property="twitter:card" content="app">
+        <meta property="twitter:url" content="{{ site_url }}">
+        <meta property="twitter:title" content="{{ title }} | Učebnice">
+        <meta property="twitter:description" content="{{ description }}">
+        {% comment %}<meta property="twitter:image" content="">{% endcomment %}
+
+        <link
+            rel="icon"
+            type="image/png"
+            href="{% static "shared/favicon.png" %}"
+        >
+
+        <link
+            href="{% static "shared/style.css" %}"
+            rel="stylesheet"
+            media="all"
+        >
+
+        <link
+            href="https://styleguide.pirati.cz/2.12.x/css/styles.css"
+            rel="stylesheet"
+            media="all"
+        >
+        <link
+            href="https://styleguide.pirati.cz/2.12.x/css/pattern-scaffolding.css"
+            rel="stylesheet"
+            media="all"
+        >
+
+        {% render_bundle "base" %}
+
+        <title>{{ title }} | Učebnice</title>
+    </head>
+    <body>
+        <nav class="navbar navbar--simple __js-root">
+            <ui-app inline-template>
+                <ui-navbar inline-template>
+                    <div>
+                        <div class="container container--default navbar__content navbar__content--initialized">
+                            <div class="navbar__brand flex items-center pr-8 my-4 lg:my-0">
+                                <a href="{% url "lectures:view_avilable_groups" %}">
+                                    <img src="https://styleguide.pirati.cz/2.12.x/images/logo-round-white.svg" class="w-8">
+                                </a>
+                                <div class="pl-4 font-bold text-xl border-r border-grey-300 pr-8">
+                                    <a href="{% url "lectures:view_avilable_groups" %}">Učebnice</a>
+                                </div>
+                                {% if header_name %}
+                                    <div class="ml-8">
+                                        {{ header_name }}
+                                    </div>
+                                {% endif %}
+                            </div>
+
+                            <div class="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto">
+                                <ul class="navbar-menu text-white">
+                                    {% if user.is_staff %}
+                                        <li class="navbar-menu__item">
+                                            <a
+                                                href="{% url "admin:index" %}"
+                                                data-href="{% url "admin:index" %}"
+                                                class="navbar-menu__link flex items-center gap-2"
+                                            >
+                                                <i class="ico--power text-sm"></i>Administrace
+                                            </a>
+                                        </li>
+                                    {% endif %}
+                                </ul>
+                            </div>
+
+                            <div class="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-col sm:flex-row lg:flex-col sm:space-x-4 space-y-2 sm:space-y-0 lg:space-y-2 xl:flex-row xl:space-x-2 xl:space-y-0">
+                                {% if user and not user.is_anonymous %}
+                                    <div class="flex items-center space-x-4">
+                                        <span class="head-heavy-2xs">{{ user.get_username }}</span>
+                                        <div class="avatar avatar--2xs">
+                                            <img
+                                                src="https://a.pirati.cz/piratar/100/{{ user.sso_username }}.jpg"
+                                                alt="Tvůj profilový obrázek"
+                                            >
+                                        </div>
+                                        <form action="{% url "oidc_logout" %}" method="POST">
+                                            {% csrf_token %}
+                                            <button
+                                                class="text-grey-200 hover:text-white __tooltipped"
+                                                type="submit"
+                                                aria-label="Odhlásit se"
+                                            ><i class="ico--log-out"></i></button>
+                                        </form>
+                                    </div>
+                                {% else %}
+                                    <a
+                                        class="btn btn--white btn--hoveractive cursor-pointer"
+                                        href="{% url "oidc_authentication_init" %}"
+                                    >
+                                        <div class="btn__body">Přihlásit se</div>
+                                    </a>
+                                {% endif %}
+                            </div>
+                        </div>
+                    </div>
+                </ui-navbar>
+            </ui-app>
+        </nav>
+
+        <div class="container container--default py-8 lg:py-24">
+            <main>
+                {% block content %}{% endblock %}
+            </main>
+        </div>
+
+        <footer class="footer bg-grey-700 text-white __js-root">
+            <div>
+                <div class="footer__main py-4 lg:py-16 container container--default">
+                    <section class="footer__brand">
+                        <a href="https://www.pirati.cz">
+                            <img
+                                src="https://styleguide.pirati.cz/2.12.x/images/logo-full-white.svg"
+                                alt="Logo Pirátské strany"
+                                class="w-32 md:w-40 pb-6"
+                            >
+                        </a>
+                        <p class="para mb-4 text-grey-200">
+                            <span class="copyleft inline-block">©</span> {% now "Y" %} Piráti.
+                            Všechna práva vyhlazena.
+                            Sdílejte a nechte ostatní sdílet za stejných podmínek.
+                        </p>
+                        <p class="para mb-6 lg:mb-0 text-grey-200">
+                            <a href="https://www.pirati.cz/ochrana-osobnich-udaju/">
+                                <span class="text-grey-200">Zásady ochrany osobních údajů</span>
+                            </a>
+                        </p>
+                    </section>
+                    <section class="footer__social lg:text-right">
+                        <div class="mb-4">
+                            <div class="social-icon-group space-x-2 text-white pb-4">
+                                <a href="https://www.pirati.cz" class="social-icon">
+                                    <i class="ico--home"></i>
+                                </a>
+                                <a href="https://www.facebook.com/ceska.piratska.strana/" class="social-icon">
+                                    <i class="ico--facebook"></i>
+                                </a>
+                                <a href="https://twitter.com/PiratskaStrana" class="social-icon">
+                                    <i class="ico--twitter"></i>
+                                </a>
+                                <a href="https://www.youtube.com/user/CeskaPiratskaStrana" class="social-icon">
+                                    <i class="ico--youtube"></i>
+                                </a>
+                                <a href="https://www.instagram.com/pirati.cz/" class="social-icon">
+                                    <i class="ico--instagram"></i>
+                                </a>
+                                <a href="https://www.flickr.com/photos/pirati/" class="social-icon">
+                                    <i class="ico--flickr"></i>
+                                </a>
+                            </div>
+                        </div>
+                        <div class="flex flex-col md:flex-row lg:flex-col lg:items-end space-y-2 md:space-y-0 md:space-x-2 lg:space-x-0 lg:space-y-2">
+                            <a href="https://dary.pirati.cz" class="btn btn--icon btn--cyan-200 btn--hoveractive text-lg btn--fullwidth sm:btn--autowidth">
+                                <div class="btn__body-wrap">
+                                    <div class="btn__body">Přispěj</div>
+                                    <div class="btn__icon "><i class="ico--pig"></i></div>
+                                </div>
+                            </a>
+                            <a href="https://nalodeni.pirati.cz" class="btn btn--icon btn--blue-300 btn--hoveractive text-lg btn--fullwidth sm:btn--autowidth">
+                                <div class="btn__body-wrap">
+                                    <div class="btn__body ">Naloď se</div>
+                                    <div class="btn__icon "><i class="ico--anchor"></i></div>
+                                </div>
+                            </a>
+                        </div>
+                    </section>
+                </div>
+            </div>
+        </footer>
+
+        <script
+            src="https://styleguide.pirati.cz/2.12.x/js/main.bundle.js"
+        ></script>
+    </body>
+</html>
diff --git a/ucebnice/settings/base.py b/ucebnice/settings/base.py
index 6c0fb6de2afb7bbac2f4e1bf0d7660fdb8e9f94b..5b858b7c941e8559b14e5deee0042cf0d001ff6c 100644
--- a/ucebnice/settings/base.py
+++ b/ucebnice/settings/base.py
@@ -139,7 +139,7 @@ LOGOUT_REDIRECT_URL = "/"
 OIDC_RP_CLIENT_ID = env.str("OIDC_RP_CLIENT_ID")
 OIDC_RP_CLIENT_SECRET = env.str("OIDC_RP_CLIENT_SECRET")
 OIDC_RP_REALM_URL = env.str("OIDC_RP_REALM_URL")
-OIDC_RP_SCOPES = "openid profile groups"
+OIDC_RP_SCOPES = "openid profile email groups"
 OIDC_RP_SIGN_ALGO = "RS256"
 OIDC_RP_RESOURCE_ACCESS_CLIENT = env.str(
     "OIDC_RESOURCE_ACCESS_CLIENT", OIDC_RP_CLIENT_ID
diff --git a/ucebnice/urls.py b/ucebnice/urls.py
index d675a06c2fc6547886b7d9424d2f105af4a13fd9..6767e3029e739daeff06b8f14ca2d009fa6875ce 100644
--- a/ucebnice/urls.py
+++ b/ucebnice/urls.py
@@ -24,5 +24,7 @@ from pirates.urls import urlpatterns as pirates_urlpatterns
 import ucebnice.admin
 
 urlpatterns = [
+    path("", include("lectures.urls")),
+    path("markdownx/", include("markdownx.urls")),
     path("admin/", admin.site.urls),
 ] + pirates_urlpatterns
diff --git a/users/admin.py b/users/admin.py
index d2e7cac9cda39bc9169098b90e87182b9559c76e..ecd2efa5a231ee8c87dbcaeeb7b87c64ba72c6bb 100644
--- a/users/admin.py
+++ b/users/admin.py
@@ -4,4 +4,9 @@ from shared.admin import MarkdownxGuardedModelAdmin
 
 from .models import User
 
-admin.site.register(User, MarkdownxGuardedModelAdmin)
+
+class UserAdmin(MarkdownxGuardedModelAdmin):
+    autocomplete_fields = ("rsvp_lectures",)
+
+
+admin.site.register(User, UserAdmin)
diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py
index ffe7ac058a29e556116fd52ec9303f4cea94cb1e..2021daf93c69bde873197f0c7679f1afdb02b2a6 100644
--- a/users/migrations/0001_initial.py
+++ b/users/migrations/0001_initial.py
@@ -1,36 +1,114 @@
 # Generated by Django 4.1.4 on 2023-04-14 18:14
 
-from django.db import migrations, models
 import django.utils.timezone
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     initial = True
 
     dependencies = [
-        ('auth', '0012_alter_user_first_name_max_length'),
+        ("auth", "0012_alter_user_first_name_max_length"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='User',
+            name="User",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
-                ('sso_id', models.CharField(error_messages={'unique': 'A user with that SSO ID already exists.'}, max_length=150, unique=True, verbose_name='SSO ID')),
-                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
-                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
-                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
-                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
-                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
-                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
-                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
-                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "is_superuser",
+                    models.BooleanField(
+                        default=False,
+                        help_text="Designates that this user has all permissions without explicitly assigning them.",
+                        verbose_name="superuser status",
+                    ),
+                ),
+                (
+                    "sso_id",
+                    models.CharField(
+                        error_messages={
+                            "unique": "A user with that SSO ID already exists."
+                        },
+                        max_length=150,
+                        unique=True,
+                        verbose_name="SSO ID",
+                    ),
+                ),
+                (
+                    "first_name",
+                    models.CharField(
+                        blank=True, max_length=150, verbose_name="first name"
+                    ),
+                ),
+                (
+                    "last_name",
+                    models.CharField(
+                        blank=True, max_length=150, verbose_name="last name"
+                    ),
+                ),
+                (
+                    "email",
+                    models.EmailField(
+                        blank=True, max_length=254, verbose_name="email address"
+                    ),
+                ),
+                (
+                    "is_staff",
+                    models.BooleanField(
+                        default=False,
+                        help_text="Designates whether the user can log into this admin site.",
+                        verbose_name="staff status",
+                    ),
+                ),
+                (
+                    "is_active",
+                    models.BooleanField(
+                        default=True,
+                        help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
+                        verbose_name="active",
+                    ),
+                ),
+                (
+                    "date_joined",
+                    models.DateTimeField(
+                        default=django.utils.timezone.now, verbose_name="date joined"
+                    ),
+                ),
+                (
+                    "groups",
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+                        related_name="user_set",
+                        related_query_name="user",
+                        to="auth.group",
+                        verbose_name="groups",
+                    ),
+                ),
+                (
+                    "user_permissions",
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text="Specific permissions for this user.",
+                        related_name="user_set",
+                        related_query_name="user",
+                        to="auth.permission",
+                        verbose_name="user permissions",
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Uživatel',
-                'verbose_name_plural': 'Uživatelé',
+                "verbose_name": "Uživatel",
+                "verbose_name_plural": "Uživatelé",
             },
         ),
     ]
diff --git a/users/migrations/0002_user_rsvp_lectures.py b/users/migrations/0002_user_rsvp_lectures.py
new file mode 100644
index 0000000000000000000000000000000000000000..642997ed060c4dadb82e46601d903a2bffc59982
--- /dev/null
+++ b/users/migrations/0002_user_rsvp_lectures.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.4 on 2023-04-18 06:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0001_initial'),
+        ('users', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='rsvp_lectures',
+            field=models.ManyToManyField(blank=True, null=True, to='lectures.lecture', verbose_name='Zaregistrované události'),
+        ),
+    ]
diff --git a/users/migrations/0003_alter_user_rsvp_lectures.py b/users/migrations/0003_alter_user_rsvp_lectures.py
new file mode 100644
index 0000000000000000000000000000000000000000..09afbba68b69868329d1d6dee92d3278f8400cda
--- /dev/null
+++ b/users/migrations/0003_alter_user_rsvp_lectures.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.4 on 2023-04-18 06:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0001_initial'),
+        ('users', '0002_user_rsvp_lectures'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='user',
+            name='rsvp_lectures',
+            field=models.ManyToManyField(blank=True, to='lectures.lecture', verbose_name='Zaregistrované události'),
+        ),
+    ]
diff --git a/users/migrations/0004_alter_user_rsvp_lectures.py b/users/migrations/0004_alter_user_rsvp_lectures.py
new file mode 100644
index 0000000000000000000000000000000000000000..1124c135dfc40f5860167bb6460a483eb2f675cf
--- /dev/null
+++ b/users/migrations/0004_alter_user_rsvp_lectures.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.4 on 2023-04-18 07:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0001_initial'),
+        ('users', '0003_alter_user_rsvp_lectures'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='user',
+            name='rsvp_lectures',
+            field=models.ManyToManyField(blank=True, to='lectures.lecture', verbose_name='Zaregistrované lekce'),
+        ),
+    ]
diff --git a/users/migrations/0005_alter_user_email.py b/users/migrations/0005_alter_user_email.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd559b82f227743e8b30988692a4e12203543d9f
--- /dev/null
+++ b/users/migrations/0005_alter_user_email.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-04-18 07:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0004_alter_user_rsvp_lectures'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='user',
+            name='email',
+            field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mailová adresa'),
+        ),
+    ]
diff --git a/users/migrations/0006_user_sso_username.py b/users/migrations/0006_user_sso_username.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0a50e5da161281bb4da47eb3506d46428da29e6
--- /dev/null
+++ b/users/migrations/0006_user_sso_username.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.4 on 2023-04-18 08:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0005_alter_user_email'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='sso_username',
+            field=models.CharField(default='', max_length=128, verbose_name='Username z SSO'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/users/migrations/0007_alter_user_rsvp_lectures.py b/users/migrations/0007_alter_user_rsvp_lectures.py
new file mode 100644
index 0000000000000000000000000000000000000000..c40dbef8760f1b4746102e122802b96936701b31
--- /dev/null
+++ b/users/migrations/0007_alter_user_rsvp_lectures.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.4 on 2023-04-18 09:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('lectures', '0005_lecturegroup'),
+        ('users', '0006_user_sso_username'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='user',
+            name='rsvp_lectures',
+            field=models.ManyToManyField(blank=True, related_name='rsvp_users', to='lectures.lecture', verbose_name='Zaregistrované lekce'),
+        ),
+    ]
diff --git a/users/models.py b/users/models.py
index f04e2e2281a18464a737e2ad578fa725a2a67bdb..eca7b683e1e95722296afb5442052409e5912d4e 100644
--- a/users/models.py
+++ b/users/models.py
@@ -17,6 +17,24 @@ class Group:
 
 
 class User(pirates_models.AbstractUser):
+    sso_username = models.CharField(
+        max_length=128,
+        verbose_name="Username z SSO",
+    )
+
+    email = models.EmailField(
+        blank=True,
+        null=True,
+        verbose_name="E-mailová adresa",
+    )
+
+    rsvp_lectures = models.ManyToManyField(
+        "lectures.Lecture",
+        blank=True,
+        related_name="rsvp_users",
+        verbose_name="Zaregistrované lekce",
+    )
+
     def set_unusable_password(self) -> None:
         # Purely for compatibility with Guardian
         pass