devnamdev2003 commited on
Commit
727a40a
·
1 Parent(s): 52f3c7c
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +53 -0
  2. .vscode/launch.json +16 -0
  3. Dockerfile +25 -0
  4. LICENSE +21 -0
  5. README.md +1 -0
  6. chat/__init__.py +0 -0
  7. chat/admin.py +44 -0
  8. chat/apps.py +5 -0
  9. chat/consumers.py +71 -0
  10. chat/models.py +32 -0
  11. chat/routing.py +6 -0
  12. chat/serializers.py +14 -0
  13. chat/static/chat/css/base.css +127 -0
  14. chat/static/chat/css/chat.css +271 -0
  15. chat/static/chat/css/color.css +65 -0
  16. chat/static/chat/css/home.css +122 -0
  17. chat/static/chat/css/login.css +169 -0
  18. chat/static/chat/css/profile.css +87 -0
  19. chat/static/chat/css/search.css +89 -0
  20. chat/static/chat/css/settings.css +136 -0
  21. chat/static/chat/css/signup.css +141 -0
  22. chat/static/chat/favicon_io/favicon.ico +0 -0
  23. chat/static/chat/favicon_io/site.webmanifest +49 -0
  24. chat/static/chat/js/base.js +39 -0
  25. chat/static/chat/js/chat.js +60 -0
  26. chat/static/chat/js/home.js +27 -0
  27. chat/static/chat/js/login.js +74 -0
  28. chat/static/chat/js/profile.js +1 -0
  29. chat/static/chat/js/search.js +1 -0
  30. chat/static/chat/js/settings.js +1 -0
  31. chat/static/chat/js/signup.js +91 -0
  32. chat/tests.py +3 -0
  33. chat/views/__init__.py +6 -0
  34. chat/views/chat_view.py +68 -0
  35. chat/views/friends_view.py +136 -0
  36. chat/views/home_view.py +110 -0
  37. chat/views/password_view.py +14 -0
  38. chat/views/test_view.py +65 -0
  39. chat/views/user_view.py +128 -0
  40. devnoms/__init__.py +0 -0
  41. devnoms/asgi.py +16 -0
  42. devnoms/settings.py +161 -0
  43. devnoms/urls.py +61 -0
  44. devnoms/wsgi.py +16 -0
  45. manage.py +22 -0
  46. requirements.txt +0 -0
  47. static/admin/css/autocomplete.css +279 -0
  48. static/admin/css/base.css +1179 -0
  49. static/admin/css/changelists.css +343 -0
  50. static/admin/css/dark_mode.css +130 -0
.gitignore ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore virtual environment
2
+ venv/
3
+ venv/*
4
+
5
+ env/
6
+ env/*
7
+
8
+ .env
9
+
10
+ # Python bytecode
11
+ *.py[cod]
12
+ __pycache__/
13
+
14
+ # Django stuff
15
+ *.log
16
+ *.pot
17
+ *.pyc
18
+ *.pyo
19
+ *.pyd
20
+ *.sqlite3
21
+ db.sqlite3
22
+
23
+ # Migrations
24
+ */migrations/*.py
25
+ */migrations/*.pyc
26
+
27
+ # Environment variables
28
+ # .env
29
+
30
+
31
+ # Static and media files (if not managed by version control)
32
+ # staticfiles/
33
+ # media/
34
+
35
+ # Ignore coverage and testing files
36
+ htmlcov/
37
+ .coverage
38
+ .tox/
39
+ nosetests.xml
40
+ coverage.xml
41
+ *.cover
42
+ *.log
43
+ *.pid
44
+ *.seed
45
+ *.manifest
46
+ *.lock
47
+
48
+ # Ignore IDE/editor settings
49
+ # .vscode/
50
+ .idea/
51
+ *.swp
52
+ *.swo
53
+ *.swn
.vscode/launch.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Run Daphne Server",
6
+ "type": "debugpy",
7
+ "request": "launch",
8
+ "program": "${workspaceFolder}/venv/Scripts/daphne.exe", // Adjusted path for Windows
9
+ "args": [
10
+ "devnoms.asgi:application"
11
+ ],
12
+ "console": "integratedTerminal"
13
+ }
14
+ ]
15
+ }
16
+
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13.5-slim
2
+
3
+ # Create non-root user (HF requirement)
4
+ RUN useradd -m -u 1000 user
5
+
6
+ # Set work directory
7
+ WORKDIR /home/user/app
8
+
9
+ # Copy requirements first (better cache)
10
+ COPY requirements.txt .
11
+
12
+ # Install dependencies
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy project files
16
+ COPY . .
17
+
18
+ # Switch to non-root user
19
+ USER user
20
+
21
+ # Expose HF port
22
+ EXPOSE 7860
23
+
24
+ # Run Django with Gunicorn (PRODUCTION SAFE)
25
+ CMD ["daphne", "devnoms.asgi:application", "--bind", "0.0.0.0","--port" , "7860"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Dev Namdev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -4,6 +4,7 @@ emoji: 🏢
4
  colorFrom: purple
5
  colorTo: red
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
 
4
  colorFrom: purple
5
  colorTo: red
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
chat/__init__.py ADDED
File without changes
chat/admin.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import UserRelation
3
+ from django.contrib import admin
4
+ from django.contrib.auth.admin import UserAdmin
5
+ from django.contrib.auth.models import User
6
+ from .models import Messages
7
+
8
+
9
+ class MessagesAdmin(admin.ModelAdmin):
10
+ list_display = ("sender_name", "receiver_name", "id" , "seen", "description")
11
+ list_filter = ("sender_name", "receiver_name", "seen")
12
+ search_fields = ("sender_name__username", "receiver_name__username", "description")
13
+
14
+
15
+ admin.site.register(Messages, MessagesAdmin)
16
+
17
+
18
+ # Define a custom admin class for the User model
19
+ class CustomUserAdmin(UserAdmin):
20
+ list_display = (
21
+ "id",
22
+ "username",
23
+ "email",
24
+ "first_name",
25
+ "last_name",
26
+ "is_staff",
27
+ "date_joined",
28
+ )
29
+
30
+
31
+ # Unregister the default UserAdmin
32
+ admin.site.unregister(User)
33
+
34
+ # Register the User model with the custom admin class
35
+ admin.site.register(User, CustomUserAdmin)
36
+
37
+
38
+ class UserRelationAdmin(admin.ModelAdmin):
39
+ list_display = ("id", "user", "friend", "accepted", "relation_key")
40
+ list_filter = ("user", "accepted")
41
+ search_fields = ("user__username", "friend")
42
+
43
+
44
+ admin.site.register(UserRelation, UserRelationAdmin)
chat/apps.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class chatConfig(AppConfig):
5
+ name = 'chat'
chat/consumers.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from channels.generic.websocket import AsyncWebsocketConsumer
3
+
4
+ from django.utils import timezone
5
+ from channels.db import database_sync_to_async
6
+
7
+ # Define an async function to save the message to the database
8
+ @database_sync_to_async
9
+ def save_message(sender_name, receiver_name, message):
10
+ from django.contrib.auth.models import User
11
+ from chat.models import Messages
12
+ # Get User instances for sender and receiver
13
+ sender = User.objects.get(username=sender_name)
14
+ receiver = User.objects.get(username=receiver_name)
15
+
16
+ # Create a new message
17
+ new_message = Messages(
18
+ description=message,
19
+ sender_name=sender,
20
+ receiver_name=receiver,
21
+ time=timezone.now().time(), # Store the current time
22
+ timestamp=timezone.now(), # Store the timestamp
23
+ seen=True
24
+ )
25
+ new_message.save()
26
+ class ChatConsumer(AsyncWebsocketConsumer):
27
+ async def connect(self):
28
+ self.room_name = self.scope['url_route']['kwargs']['room_name']
29
+ self.room_group_name = f'chat_{self.room_name}'
30
+ # print(self.room_name, self.room_group_name)
31
+ # Join room group
32
+ await self.channel_layer.group_add(
33
+ self.room_group_name,
34
+ self.channel_name
35
+ )
36
+ await self.accept()
37
+
38
+ async def disconnect(self, close_code):
39
+ # Leave room group
40
+ await self.channel_layer.group_discard(
41
+ self.room_group_name,
42
+ self.channel_name
43
+ )
44
+
45
+ # Receive message from WebSocket
46
+ async def receive(self, text_data):
47
+ text_data_json = json.loads(text_data)
48
+ message = text_data_json['message']
49
+ sender_name = text_data_json['sender_name']
50
+ receiver_name = text_data_json['receiver_name']
51
+ await save_message(sender_name, receiver_name, message)
52
+ # Send message to room group
53
+ await self.channel_layer.group_send(
54
+ self.room_group_name,
55
+ {
56
+ 'type': 'chat_message',
57
+ 'message': message,
58
+ 'sender': sender_name
59
+ }
60
+ )
61
+
62
+ # Receive message from room group
63
+ async def chat_message(self, event):
64
+ message = event['message']
65
+ sender = event['sender']
66
+
67
+ # Send message to WebSocket
68
+ await self.send(text_data=json.dumps({
69
+ 'message': message,
70
+ 'sender': sender
71
+ }))
chat/models.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.db import models
2
+ from django.contrib.auth.models import User
3
+ from django.utils import timezone
4
+
5
+
6
+ class UserRelation(models.Model):
7
+ user = models.ForeignKey(
8
+ User, on_delete=models.CASCADE, related_name="user_relations"
9
+ )
10
+ friend = models.ForeignKey(
11
+ User, on_delete=models.CASCADE, related_name="friend_relations", default=None
12
+ )
13
+ accepted = models.BooleanField(default=False)
14
+ relation_key = models.CharField(max_length=255, blank=True, null=True) # Add relation_key field
15
+ def __str__(self):
16
+ return f"{self.user.username} - {self.friend.username}"
17
+
18
+
19
+ class Messages(models.Model):
20
+ description = models.TextField()
21
+ sender_name = models.ForeignKey(
22
+ User, on_delete=models.CASCADE, related_name="sender"
23
+ )
24
+ receiver_name = models.ForeignKey(
25
+ User, on_delete=models.CASCADE, related_name="receiver"
26
+ )
27
+ time = models.TimeField(auto_now_add=True)
28
+ seen = models.BooleanField(default=False)
29
+ timestamp = models.DateTimeField(default=timezone.now, blank=True)
30
+
31
+ class Meta:
32
+ ordering = ("timestamp",)
chat/routing.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from django.urls import path
2
+ from . import consumers
3
+
4
+ websocket_urlpatterns = [
5
+ path('ws/chat/<str:room_name>/', consumers.ChatConsumer.as_asgi()),
6
+ ]
chat/serializers.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from rest_framework import serializers
2
+ from .models import Messages
3
+ from django.contrib.auth.models import User
4
+
5
+
6
+
7
+ class MessageSerializer(serializers.ModelSerializer):
8
+
9
+ sender_name = serializers.SlugRelatedField(many=False, slug_field='username', queryset=User.objects.all())
10
+ receiver_name = serializers.SlugRelatedField(many=False, slug_field='username', queryset=User.objects.all())
11
+
12
+ class Meta:
13
+ model = Messages
14
+ fields = ['sender_name', 'receiver_name', 'description', 'time']
chat/static/chat/css/base.css ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ::-webkit-scrollbar {
2
+ width: 5px;
3
+ }
4
+
5
+ ::-webkit-scrollbar-thumb {
6
+ background-color: var(--scrollbar-thumb);
7
+ border-radius: 50px;
8
+ }
9
+
10
+ ::-webkit-scrollbar-track {
11
+ background-color: var(--scrollbar-track);
12
+ }
13
+
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ -webkit-tap-highlight-color: transparent;
19
+ }
20
+
21
+ html,
22
+ body {
23
+ font-family: 'Roboto', sans-serif;
24
+ background: var(--bg-gradient);
25
+ color: var(--text-color);
26
+ min-height: 100%;
27
+ animation: gradientMove var(--gradient-speed) infinite;
28
+ }
29
+
30
+ .main_box {
31
+ display: flex;
32
+ justify-content: center;
33
+ }
34
+
35
+ .header {
36
+ display: flex;
37
+ justify-content: space-between;
38
+ align-items: center;
39
+ width: 100%;
40
+ max-width: 450px;
41
+ background-color: var(--menu-bg);
42
+ padding: 15px;
43
+ box-shadow: 0 2px 8px var(--shadow-color);
44
+ position: relative;
45
+ }
46
+
47
+ .header h1 {
48
+ color: var(--text-color);
49
+ font-size: 18px;
50
+ text-align: center;
51
+ flex: 1;
52
+ margin: 0;
53
+ }
54
+
55
+ .header button {
56
+ background: transparent;
57
+ border: none;
58
+ color: var(--text-color);
59
+ cursor: pointer;
60
+ }
61
+
62
+ .header button img {
63
+ width: 24px;
64
+ height: 24px;
65
+ filter: var(--icon-color);
66
+ }
67
+
68
+ .menu {
69
+ position: relative;
70
+ }
71
+
72
+ .menu button {
73
+ background: transparent;
74
+ border: none;
75
+ cursor: pointer;
76
+ }
77
+
78
+ .dropdown-content {
79
+ display: none;
80
+ position: absolute;
81
+ right: 0;
82
+ top: 40px;
83
+ background-color: var(--menu-bg);
84
+ min-width: 160px;
85
+ box-shadow: 0 8px 16px var(--shadow-color);
86
+ z-index: 1;
87
+ border-radius: 5px;
88
+ opacity: 0;
89
+ transition: opacity 0.3s ease;
90
+ }
91
+
92
+ .dropdown-content a {
93
+ color: var(--text-color);
94
+ padding: 10px 15px;
95
+ text-decoration: none;
96
+ border-radius: 5px;
97
+ display: block;
98
+ font-size: 16px;
99
+ }
100
+
101
+ .dropdown-content a:hover {
102
+ background-color: var(--highlight-color);
103
+ border-radius: 5px;
104
+ color: var(--bg-color);
105
+ }
106
+
107
+ .dropdown-content.show {
108
+ display: block;
109
+ border-radius: 5px;
110
+ opacity: 1;
111
+ }
112
+
113
+ .btn {
114
+ background: var(--btn-bg);
115
+ color: var(--btn-text-color);
116
+ padding: 6px 15px;
117
+ border: none;
118
+ border-radius: 20px;
119
+ cursor: pointer;
120
+ font-size: 14px;
121
+ transition: background 0.3s ease, transform 0.2s ease;
122
+ }
123
+
124
+ .btn:hover {
125
+ background: var(--btn-bg-hover);
126
+ transform: scale(1.02);
127
+ }
chat/static/chat/css/chat.css ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --theme-color: #8f91ea;
3
+ --timestamp-color: #b5b5b5;
4
+ --status-online: #28a745;
5
+ --status-offline: #888888;
6
+ --bg-gradient: linear-gradient(135deg, #121212, #1c1c1c);
7
+ --bg-color: #121212;
8
+ --text-color: #f5f5f5;
9
+ --menu-bg: #1c1c1c;
10
+ --menu-text-color: #8f91ea;
11
+ --shadow-color: rgba(0, 0, 0, 0.5);
12
+ --icon-color: invert(100%);
13
+ --highlight-color: #8f91ea;
14
+ --friend-item-hover: #2b2b2b;
15
+ --btn-text-color: #ffffff;
16
+ --btn-bg: linear-gradient(135deg, #6a75c9, #5a67c8);
17
+ --btn-bg-hover: linear-gradient(135deg, #5a67c8, #4854c8);
18
+ --btn-bg-danger: linear-gradient(135deg, #e57373, #c0392b);
19
+ --btn-bg-danger-hover: linear-gradient(135deg, #c0392b, #96281b);
20
+ --scrollbar-thumb: #444444;
21
+ --scrollbar-track: #1c1c1c;
22
+ --bg-slider-color: rgba(143, 145, 234, 0.1);
23
+ --switch-slider: #555555;
24
+ --switch-slider-before: #ffffff;
25
+ --switch-slider-checked: #8f91ea;
26
+ }
27
+
28
+ [data-theme='light'] {
29
+ --theme-color: #8f91ea;
30
+ --timestamp-color: #6c757d;
31
+ --status-online: #28a745;
32
+ --status-offline: #adb5bd;
33
+ --bg-gradient: linear-gradient(135deg, #ffffff, #f4f4f4);
34
+ --bg-color: #ffffff;
35
+ --text-color: #212529;
36
+ --menu-bg: #e9ecef;
37
+ --menu-text-color: #8f91ea;
38
+ --shadow-color: rgba(0, 0, 0, 0.1);
39
+ --icon-color: none;
40
+ --highlight-color: #8f91ea;
41
+ --friend-item-hover: #f1f3f5;
42
+ --btn-text-color: #ffffff;
43
+ --btn-bg: linear-gradient(135deg, #6a75c9, #5a67c8);
44
+ --btn-bg-hover: linear-gradient(135deg, #5a67c8, #4854c8);
45
+ --btn-bg-danger: linear-gradient(135deg, #e57373, #c0392b);
46
+ --btn-bg-danger-hover: linear-gradient(135deg, #c0392b, #96281b);
47
+ --scrollbar-thumb: #cccccc;
48
+ --scrollbar-track: #f7f7f7;
49
+ --bg-slider-color: rgba(143, 145, 234, 0.1);
50
+ --switch-slider: #cccccc;
51
+ --switch-slider-before: #ffffff;
52
+ --switch-slider-checked: #8f91ea;
53
+ }
54
+
55
+ ::-webkit-scrollbar {
56
+ width: 5px;
57
+ }
58
+
59
+ ::-webkit-scrollbar-thumb {
60
+ background-color: var(--scrollbar-thumb);
61
+ border-radius: 50px;
62
+ }
63
+
64
+ ::-webkit-scrollbar-track {
65
+ background-color: var(--scrollbar-track);
66
+ }
67
+
68
+ * {
69
+ margin: 0;
70
+ padding: 0;
71
+ box-sizing: border-box;
72
+ -webkit-tap-highlight-color: transparent;
73
+ }
74
+
75
+ html,
76
+ body {
77
+ font-family: 'Roboto', sans-serif;
78
+ background: var(--bg-gradient);
79
+ color: var(--text-color);
80
+ height: 100%;
81
+ }
82
+
83
+ .main_box {
84
+ display: flex;
85
+ justify-content: center;
86
+ }
87
+
88
+ .header {
89
+ display: flex;
90
+ justify-content: space-between;
91
+ align-items: center;
92
+ width: 100%;
93
+ max-width: 450px;
94
+ background-color: var(--menu-bg);
95
+ padding: 15px;
96
+ box-shadow: 0 2px 8px var(--shadow-color);
97
+ position: relative;
98
+ }
99
+
100
+ .header h1 {
101
+ color: var(--text-color);
102
+ font-size: 18px;
103
+ text-align: center;
104
+ flex: 1;
105
+ margin: 0;
106
+ }
107
+
108
+ .header button {
109
+ background: transparent;
110
+ border: none;
111
+ color: var(--text-color);
112
+ cursor: pointer;
113
+ }
114
+
115
+ .header button img {
116
+ width: 24px;
117
+ height: 24px;
118
+ filter: var(--icon-color);
119
+ }
120
+
121
+ .menu {
122
+ position: relative;
123
+ }
124
+
125
+ .menu button {
126
+ background: transparent;
127
+ border: none;
128
+ cursor: pointer;
129
+ }
130
+
131
+ .dropdown-content {
132
+ display: none;
133
+ position: absolute;
134
+ right: 0;
135
+ top: 40px;
136
+ background-color: var(--menu-bg);
137
+ min-width: 160px;
138
+ box-shadow: 0 8px 16px var(--shadow-color);
139
+ z-index: 1;
140
+ border-radius: 5px;
141
+ opacity: 0;
142
+ transition: opacity 0.3s ease;
143
+ }
144
+
145
+ .dropdown-content a {
146
+ color: var(--text-color);
147
+ padding: 10px 15px;
148
+ text-decoration: none;
149
+ border-radius: 5px;
150
+ display: block;
151
+ font-size: 16px;
152
+ }
153
+
154
+ .dropdown-content a:hover {
155
+ background-color: var(--highlight-color);
156
+ border-radius: 5px;
157
+ color: var(--bg-color);
158
+ }
159
+
160
+ .dropdown-content.show {
161
+ display: block;
162
+ border-radius: 5px;
163
+ opacity: 1;
164
+ }
165
+
166
+ .btn {
167
+ background: var(--btn-bg);
168
+ color: var(--btn-text-color);
169
+ padding: 6px 15px;
170
+ border: none;
171
+ border-radius: 20px;
172
+ cursor: pointer;
173
+ font-size: 14px;
174
+ transition: background 0.3s ease, transform 0.2s ease;
175
+ }
176
+
177
+ .btn:hover {
178
+ background: var(--btn-bg-hover);
179
+ transform: scale(1.02);
180
+ }
181
+
182
+ .chat_box {
183
+ background: var(--bg-color);
184
+ width: 100%;
185
+ max-width: 450px;
186
+ max-height: 900px;
187
+ display: flex;
188
+ height: calc(var(--vh, 1vh) * 100);
189
+ flex-direction: column;
190
+ box-shadow: 0 8px 16px var(--shadow-color);
191
+ }
192
+
193
+ .chat-area {
194
+ flex: 1;
195
+ padding: 10px;
196
+ overflow-y: auto;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 10px;
200
+ }
201
+
202
+ .chat-message {
203
+ display: flex;
204
+ flex-direction: column;
205
+ max-width: 70%;
206
+ }
207
+
208
+ .chat-message.sent {
209
+ align-self: flex-end;
210
+ text-align: right;
211
+ }
212
+
213
+ .chat-message.received {
214
+ align-self: flex-start;
215
+ text-align: left;
216
+ }
217
+
218
+ .message-text {
219
+ background-color: var(--menu-bg);
220
+ color: var(--text-color);
221
+ padding: 10px 15px;
222
+ border-radius: 10px;
223
+ word-wrap: break-word;
224
+ line-height: 1.5;
225
+ text-align: left;
226
+ }
227
+
228
+ .timestamp {
229
+ font-size: 12px;
230
+ color: var(--timestamp-color);
231
+ margin-top: 4px;
232
+ }
233
+
234
+ .input-area {
235
+ display: flex;
236
+ align-items: center;
237
+ border-radius: 30px;
238
+ }
239
+
240
+ .message-input {
241
+ flex: 1;
242
+ background-color: var(--menu-bg);
243
+ color: var(--text-color);
244
+ border: 8px solid var(--menu-bg);
245
+ resize: none;
246
+ outline: none;
247
+ border-radius: 20px;
248
+ height: 46px;
249
+ max-height: 110px;
250
+ overflow-y: auto;
251
+ line-height: 20px;
252
+ padding: 5px 15px;
253
+ margin: 10px 0px 10px 10px;
254
+ }
255
+
256
+ .send-button {
257
+ margin: 10px 10px 10px 5px;
258
+ padding: 5px 12px 6px 12px;
259
+ border: none;
260
+ border-radius: 50%;
261
+ background: var(--btn-bg);
262
+ color: var(--btn-text-color);
263
+ cursor: pointer;
264
+ font-size: 25px;
265
+ transition: background 0.3s ease, transform 0.2s ease;
266
+ }
267
+
268
+ .send-button:hover {
269
+ background: var(--btn-bg-hover);
270
+ transform: scale(1.02);
271
+ }
chat/static/chat/css/color.css ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --theme-color: #8f91ea;
3
+ --timestamp-color: #b5b5b5;
4
+ --status-online: #28a745;
5
+ --status-offline: #888888;
6
+ --bg-gradient: linear-gradient(135deg, #121212, #1c1c1c);
7
+ --bg-color: #121212;
8
+ --text-color: #f5f5f5;
9
+ --menu-bg: #1c1c1c;
10
+ --menu-text-color: #8f91ea;
11
+ --shadow-color: rgba(0, 0, 0, 0.5);
12
+ --icon-color: invert(100%);
13
+ --highlight-color: #8f91ea;
14
+ --friend-item-hover: #2b2b2b;
15
+ --btn-text-color: #ffffff;
16
+ --btn-bg: linear-gradient(135deg, #6a75c9, #5a67c8);
17
+ --btn-bg-hover: linear-gradient(135deg, #5a67c8, #4854c8);
18
+ --btn-bg-danger: linear-gradient(135deg, #e57373, #c0392b);
19
+ --btn-bg-danger-hover: linear-gradient(135deg, #c0392b, #96281b);
20
+ --scrollbar-thumb: #444444;
21
+ --scrollbar-track: #1c1c1c;
22
+ --bg-slider-color: rgba(143, 145, 234, 0.1);
23
+ --switch-slider: #555555;
24
+ --switch-slider-before: #ffffff;
25
+ --switch-slider-checked: #8f91ea;
26
+ --error-message: red;
27
+ --spinner-border: #f3f3f3;
28
+ --gradient-speed: 5s;
29
+ --input-border: #6a75c9;
30
+ --input-focus-border: #8f91ea;
31
+ --bg-gradient: linear-gradient(45deg, #1c1e3a, #2a2d4b, #3c3f5b);
32
+ }
33
+
34
+ [data-theme="light"] {
35
+ --theme-color: #8f91ea;
36
+ --timestamp-color: #6c757d;
37
+ --status-online: #28a745;
38
+ --status-offline: #adb5bd;
39
+ --bg-gradient: linear-gradient(135deg, #ffffff, #f4f4f4);
40
+ --bg-color: #ffffff;
41
+ --text-color: #212529;
42
+ --menu-bg: #e9ecef;
43
+ --menu-text-color: #8f91ea;
44
+ --shadow-color: rgba(0, 0, 0, 0.1);
45
+ --icon-color: none;
46
+ --highlight-color: #8f91ea;
47
+ --friend-item-hover: #f1f3f5;
48
+ --btn-text-color: #ffffff;
49
+ --btn-bg: linear-gradient(135deg, #6a75c9, #5a67c8);
50
+ --btn-bg-hover: linear-gradient(135deg, #5a67c8, #4854c8);
51
+ --btn-bg-danger: linear-gradient(135deg, #e57373, #c0392b);
52
+ --btn-bg-danger-hover: linear-gradient(135deg, #c0392b, #96281b);
53
+ --scrollbar-thumb: #cccccc;
54
+ --scrollbar-track: #f7f7f7;
55
+ --bg-slider-color: rgba(143, 145, 234, 0.1);
56
+ --switch-slider: #cccccc;
57
+ --switch-slider-before: #ffffff;
58
+ --switch-slider-checked: #8f91ea;
59
+ --error-message: red;
60
+ --spinner-border: #f3f3f3;
61
+ --gradient-speed: 5s;
62
+ --input-border: #6a75c9;
63
+ --input-focus-border: #8f91ea;
64
+ --bg-gradient: linear-gradient(45deg, #1c1e3a, #2a2d4b, #3c3f5b);
65
+ }
chat/static/chat/css/home.css ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .home_box {
2
+ background: var(--bg-color);
3
+ border-radius: 20px;
4
+ width: 100%;
5
+ max-width: 450px;
6
+ max-height: 900px;
7
+ display: flex;
8
+ height: calc(var(--vh, 1vh) * 100);
9
+ flex-direction: column;
10
+ box-shadow: 0 8px 16px var(--shadow-color);
11
+ }
12
+
13
+ .friends-list {
14
+ flex: 1;
15
+ background-color: var(--bg-color);
16
+ margin-top: 1px;
17
+ overflow-y: auto;
18
+
19
+ }
20
+
21
+ .friend-item {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ padding: 14px;
26
+ border-bottom: 1px solid var(--shadow-color);
27
+ cursor: pointer;
28
+ transition: background-color 0.3s ease;
29
+ }
30
+
31
+ .friend-item:hover {
32
+ background-color: var(--friend-item-hover);
33
+ }
34
+
35
+ .friend-item .left {
36
+ display: flex;
37
+ align-items: center;
38
+ }
39
+
40
+ .friend-item img {
41
+ width: 45px;
42
+ height: 45px;
43
+ border-radius: 50%;
44
+ margin-right: 15px;
45
+ }
46
+
47
+ .friend-item .details {
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+
52
+ .friend-item .details p {
53
+ margin: 2px 0;
54
+ color: var(--text-color);
55
+ }
56
+
57
+ .friend-item .details .name {
58
+ font-weight: bold;
59
+ font-size: 16px;
60
+ }
61
+
62
+ .friend-item .details .last-message {
63
+ font-size: 14px;
64
+ color: var(--menu-text-color);
65
+ }
66
+
67
+ .friend-item .right {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ margin-left: auto;
72
+ }
73
+
74
+ .status {
75
+ width: 12px;
76
+ height: 12px;
77
+ border-radius: 50%;
78
+ }
79
+
80
+ .status.online {
81
+ background-color: var(--status-online);
82
+ }
83
+
84
+ .status.offline {
85
+ background-color: var(--status-offline);
86
+ }
87
+
88
+ .bottom-nav {
89
+ display: flex;
90
+ justify-content: space-around;
91
+ align-items: center;
92
+ background-color: var(--menu-bg);
93
+ padding: 15px;
94
+ box-shadow: 0 -4px 8px var(--shadow-color);
95
+ position: relative;
96
+ }
97
+
98
+ .bottom-nav .slider {
99
+ position: absolute;
100
+ width: 60px;
101
+ height: 60px;
102
+ background-color: var(--bg-slider-color);
103
+ border-radius: 50%;
104
+ top: 50%;
105
+ transform: translateY(-50%);
106
+ z-index: 0;
107
+ transition: all 0.3s ease;
108
+ }
109
+
110
+ .bottom-nav button {
111
+ background: transparent;
112
+ border: none;
113
+ cursor: pointer;
114
+ z-index: 1;
115
+ position: relative;
116
+ }
117
+
118
+ .bottom-nav img {
119
+ width: 30px;
120
+ height: 30px;
121
+ filter: var(--icon-color);
122
+ }
chat/static/chat/css/login.css ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .main_box {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ height: 100vh;
6
+ }
7
+
8
+ .login-box {
9
+ background: var(--menu-bg);
10
+ width: 95%;
11
+ max-width: 450px;
12
+ display: flex;
13
+ flex-direction: column;
14
+ box-shadow: 0 8px 16px var(--shadow-color);
15
+ padding: 20px;
16
+ }
17
+
18
+ .logo img {
19
+ width: 100px;
20
+ margin: 20px;
21
+ display: block;
22
+ margin-left: auto;
23
+ margin-right: auto;
24
+ }
25
+
26
+ .login-box h2 {
27
+ color: var(--theme-color);
28
+ font-size: 24px;
29
+ margin-bottom: 20px;
30
+ text-align: center;
31
+ }
32
+
33
+ .form-group {
34
+ margin-bottom: 20px;
35
+ text-align: left;
36
+ }
37
+
38
+ .form-group label {
39
+ font-size: 14px;
40
+ color: var(--text-color);
41
+ margin-bottom: 8px;
42
+ display: block;
43
+ }
44
+
45
+ .input-group {
46
+ position: relative;
47
+ width: 100%;
48
+ }
49
+
50
+ .input-group input {
51
+ width: 100%;
52
+ padding: 12px;
53
+ font-size: 16px;
54
+ border: 2px solid var(--input-border);
55
+ border-radius: 6px;
56
+ background: var(--bg-color);
57
+ color: var(--text-color);
58
+ transition: border 0.3s;
59
+ }
60
+
61
+ .input-group input:focus {
62
+ outline: none;
63
+ border-color: var(--input-focus-border);
64
+ }
65
+
66
+ .eye-icon {
67
+ position: absolute;
68
+ top: 50%;
69
+ right: 10px;
70
+ transform: translateY(-50%);
71
+ cursor: pointer;
72
+ z-index: 1;
73
+ }
74
+
75
+ .eye-icon img {
76
+ width: 20px;
77
+ height: 20px;
78
+ filter: var(--icon-color);
79
+
80
+ }
81
+
82
+ .login-btn button {
83
+ background: var(--btn-bg);
84
+ color: var(--btn-text-color);
85
+ padding: 16px;
86
+ border: none;
87
+ border-radius: 6px;
88
+ cursor: pointer;
89
+ font-size: 16px;
90
+ width: 100%;
91
+ transition: background 0.3s ease, transform 0.2s ease;
92
+ }
93
+
94
+ .login-btn button:hover {
95
+ background: var(--btn-bg-hover);
96
+ transform: scale(1.01);
97
+ }
98
+
99
+ .error-message {
100
+ color: var(--error-message);
101
+ font-size: 14px;
102
+ text-align: center;
103
+ margin-top: 10px;
104
+ text-align: left;
105
+ }
106
+
107
+ .forgot-password {
108
+ font-size: 14px;
109
+ text-align: center;
110
+ margin: 20px;
111
+ }
112
+
113
+ .forgot-password a {
114
+ color: var(--highlight-color);
115
+ text-decoration: none;
116
+ }
117
+
118
+ .forgot-password a:hover {
119
+ text-decoration: underline;
120
+ }
121
+
122
+ .register-link {
123
+ margin: 20px;
124
+ font-size: 14px;
125
+ text-align: center;
126
+ }
127
+
128
+ .register-link a {
129
+ color: var(--highlight-color);
130
+ text-decoration: none;
131
+ }
132
+
133
+ .register-link a:hover {
134
+ text-decoration: underline;
135
+ }
136
+
137
+ .spinner {
138
+ border: 3px solid var(--spinner-border);
139
+ border-top: 3px solid var(--theme-color);
140
+ border-radius: 50%;
141
+ width: 14px;
142
+ height: 14px;
143
+ animation: spin 1s linear infinite;
144
+ display: none;
145
+ margin-left: 5px;
146
+ }
147
+
148
+ @keyframes spin {
149
+ 0% {
150
+ transform: rotate(0deg);
151
+ }
152
+
153
+ 100% {
154
+ transform: rotate(360deg);
155
+ }
156
+ }
157
+
158
+ .remember-me {
159
+ font-size: 14px;
160
+ color: var(--text-color);
161
+ margin-bottom: 20px;
162
+ }
163
+
164
+ .remember-me label {
165
+ font-size: 14px;
166
+ color: var(--text-color);
167
+ margin-bottom: 8px;
168
+ display: inline-block;
169
+ }
chat/static/chat/css/profile.css ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .profile_box {
2
+ background: var(--bg-color);
3
+ width: 100%;
4
+ max-width: 450px;
5
+ max-height: 900px;
6
+ display: flex;
7
+ height: calc(var(--vh, 1vh) * 100);
8
+ flex-direction: column;
9
+ box-shadow: 0 8px 16px var(--shadow-color);
10
+ }
11
+
12
+ .profile-container {
13
+ width: 100%;
14
+ padding: 50px 20px;
15
+ text-align: center;
16
+ height: 100%;
17
+ }
18
+
19
+ .profile-image {
20
+ width: 100px;
21
+ height: 100px;
22
+ border-radius: 50%;
23
+ margin: 0 auto;
24
+ border: 4px solid var(--highlight-color);
25
+ }
26
+
27
+ .username {
28
+ font-size: 18px;
29
+ font-weight: bold;
30
+ margin-top: 10px;
31
+ color: var(--highlight-color);
32
+ }
33
+
34
+ .user-status {
35
+ font-size: 14px;
36
+ color: var(--text-color);
37
+ margin-top: 5px;
38
+ }
39
+
40
+ .status-online {
41
+ color: var(--status-online)
42
+ }
43
+
44
+ .status-offline {
45
+ color: var(--status-offline);
46
+ }
47
+
48
+ .bio {
49
+ margin-top: 15px;
50
+ font-size: 16px;
51
+ color: var(--text-color);
52
+ text-align: center;
53
+ font-style: italic;
54
+ }
55
+
56
+ .profile-stats {
57
+ display: flex;
58
+ justify-content: space-around;
59
+ margin-top: 15px;
60
+ font-size: 16px;
61
+ color: var(--text-color);
62
+ }
63
+
64
+ .profile-stats div {
65
+ text-align: center;
66
+ }
67
+
68
+ .profile-stats div strong {
69
+ font-size: 18px;
70
+ color: var(--highlight-color);
71
+ }
72
+
73
+ .user-details {
74
+ margin-top: 20px;
75
+ text-align: left;
76
+ }
77
+
78
+ .user-details p {
79
+ margin: 10px 0;
80
+ font-size: 16px;
81
+ color: var(--text-color);
82
+ }
83
+
84
+ .user-details span {
85
+ font-weight: bold;
86
+ color: var(--highlight-color);
87
+ }
chat/static/chat/css/search.css ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .search_box {
2
+ background: var(--bg-color);
3
+ border-radius: 20px;
4
+ width: 100%;
5
+ max-width: 450px;
6
+ max-height: 900px;
7
+ display: flex;
8
+ height: calc(var(--vh, 1vh) * 100);
9
+ flex-direction: column;
10
+ box-shadow: 0 8px 16px var(--shadow-color);
11
+ }
12
+
13
+
14
+
15
+ .search-bar {
16
+ display: flex;
17
+ align-items: center;
18
+ margin: 15px;
19
+ background: var(--friend-item-hover);
20
+ padding: 10px;
21
+ border-radius: 10px;
22
+ }
23
+
24
+ .search-bar input {
25
+ flex: 1;
26
+ padding: 8px 10px;
27
+ border: none;
28
+ background: transparent;
29
+ color: var(--text-color);
30
+ font-size: 16px;
31
+ outline: none;
32
+ }
33
+
34
+ .search-bar button {
35
+ background: transparent;
36
+ border: none;
37
+ cursor: pointer;
38
+ display: flex;
39
+ align-items: center;
40
+ }
41
+
42
+ .search-bar img {
43
+ width: 24px;
44
+ height: 24px;
45
+ filter: var(--icon-color);
46
+ }
47
+
48
+ .results-list {
49
+ flex: 1;
50
+ background-color: var(--bg-color);
51
+ margin-top: 1px;
52
+ overflow-y: auto;
53
+ }
54
+
55
+ .result-item {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ padding: 14px;
60
+ border-bottom: 1px solid var(--shadow-color);
61
+ cursor: pointer;
62
+ transition: background-color 0.3s ease;
63
+ transition: transform 0.3s ease;
64
+ }
65
+
66
+ .result-item:hover {
67
+ background-color: var(--friend-item-hover);
68
+ }
69
+
70
+ .result-item img {
71
+ width: 45px;
72
+ height: 45px;
73
+ border-radius: 50%;
74
+ margin-right: 15px;
75
+ }
76
+
77
+ .result-item .details {
78
+ flex: 1;
79
+ }
80
+
81
+ .result-item .details p {
82
+ margin: 2px 0;
83
+ color: var(--text-color);
84
+ }
85
+
86
+ .result-item .details .name {
87
+ font-weight: bold;
88
+ font-size: 16px;
89
+ }
chat/static/chat/css/settings.css ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .settings_box {
2
+ background: var(--bg-color);
3
+ border-radius: 20px;
4
+ width: 100%;
5
+ max-width: 450px;
6
+ max-height: 900px;
7
+ display: flex;
8
+ height: calc(var(--vh, 1vh) * 100);
9
+ flex-direction: column;
10
+ box-shadow: 0 8px 16px var(--shadow-color);
11
+ }
12
+
13
+ .settings-options {
14
+ flex: 1;
15
+ background-color: var(--bg-color);
16
+ margin-top: 1px;
17
+ overflow-y: auto;
18
+ }
19
+
20
+ .section-title {
21
+ padding: 15px 15px 15px 14px;
22
+ font-size: 24px;
23
+ color: var(--text-color);
24
+ font-weight: bold;
25
+ }
26
+
27
+ .settings-option {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ padding: 14px;
32
+ border-bottom: 1px solid var(--shadow-color);
33
+ cursor: pointer;
34
+ transition: background-color 0.3s ease;
35
+ }
36
+
37
+ .settings-option:hover {
38
+ background-color: var(--menu-bg);
39
+ }
40
+
41
+ .settings-option label {
42
+ color: var(--text-color);
43
+ font-size: 16px;
44
+ }
45
+
46
+ .settings-option input {
47
+ margin-right: 10px;
48
+ }
49
+
50
+ .setting-options .switch {
51
+ position: relative;
52
+ display: inline-block;
53
+ width: 47px;
54
+ height: 28px;
55
+ }
56
+
57
+ .setting-options .switch input {
58
+ opacity: 0;
59
+ width: 0;
60
+ height: 0;
61
+ }
62
+
63
+ .setting-options .slider {
64
+ position: absolute;
65
+ cursor: pointer;
66
+ top: 0;
67
+ left: 0;
68
+ right: 0;
69
+ bottom: 0;
70
+ background-color: var(--switch-slider);
71
+ transition: 0.4s;
72
+ border-radius: 34px;
73
+ }
74
+
75
+ .setting-options .slider:before {
76
+ position: absolute;
77
+ content: "";
78
+ height: 20px;
79
+ width: 20px;
80
+ border-radius: 50%;
81
+ left: 4px;
82
+ bottom: 4px;
83
+ background-color: var(--switch-slider-before);
84
+ transition: 0.4s;
85
+ }
86
+
87
+ .setting-options input:checked+.slider {
88
+ background-color: var(--switch-slider-checked);
89
+ }
90
+
91
+ .setting-options input:checked+.slider:before {
92
+ background-color: var(--switch-slider-before);
93
+ transform: translateX(20px);
94
+ }
95
+
96
+ .settings-option select {
97
+ background-color: var(--bg-color);
98
+ color: var(--text-color);
99
+ border: 1px solid var(--highlight-color);
100
+ padding: 10px;
101
+ font-size: 16px;
102
+ border-radius: 5px;
103
+ outline: none;
104
+ transition: background-color 0.3s ease, color 0.3s ease;
105
+ }
106
+
107
+ .settings-option select:focus {
108
+ background-color: var(--menu-bg);
109
+ color: var(--highlight-color);
110
+ }
111
+
112
+ .settings-option option {
113
+ background-color: var(--bg-color);
114
+ color: var(--text-color);
115
+ }
116
+
117
+ .settings-option select:hover {
118
+ cursor: pointer;
119
+ border-color: var(--highlight-color);
120
+ }
121
+
122
+ .btn {
123
+ background: var(--btn-bg-danger);
124
+ color: var(--btn-text-color);
125
+ padding: 6px 15px;
126
+ border: none;
127
+ border-radius: 20px;
128
+ cursor: pointer;
129
+ font-size: 14px;
130
+ transition: background 0.3s ease, transform 0.2s ease;
131
+ }
132
+
133
+ .btn:hover {
134
+ background: var(--btn-bg-danger-hover);
135
+ transform: scale(1.02);
136
+ }
chat/static/chat/css/signup.css ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .main_box {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ height: 100vh;
6
+ }
7
+
8
+ .signup-box {
9
+ background: var(--menu-bg);
10
+ width: 99%;
11
+ max-width: 450px;
12
+ display: flex;
13
+ flex-direction: column;
14
+ box-shadow: 0 8px 16px var(--shadow-color);
15
+ padding: 20px;
16
+ }
17
+
18
+ .logo img {
19
+ width: 100px;
20
+ margin: 20px;
21
+ display: block;
22
+ margin-left: auto;
23
+ margin-right: auto;
24
+ }
25
+
26
+ .signup-box h2 {
27
+ color: var(--theme-color);
28
+ font-size: 24px;
29
+ margin-bottom: 20px;
30
+ text-align: center;
31
+ }
32
+
33
+ .form-group {
34
+ margin-bottom: 20px;
35
+ text-align: left;
36
+ }
37
+
38
+ .form-group label {
39
+ font-size: 14px;
40
+ color: var(--text-color);
41
+ margin-bottom: 8px;
42
+ display: block;
43
+ }
44
+
45
+ .input-group {
46
+ position: relative;
47
+ width: 100%;
48
+ }
49
+
50
+ .input-group input {
51
+ width: 100%;
52
+ padding: 12px;
53
+ font-size: 16px;
54
+ border: 2px solid var(--input-border);
55
+ border-radius: 6px;
56
+ background: var(--bg-color);
57
+ color: var(--text-color);
58
+ transition: border 0.3s;
59
+ }
60
+
61
+ .input-group input:focus {
62
+ outline: none;
63
+ border-color: var(--input-focus-border);
64
+ }
65
+
66
+ .eye-icon {
67
+ position: absolute;
68
+ top: 50%;
69
+ right: 10px;
70
+ transform: translateY(-50%);
71
+ cursor: pointer;
72
+ z-index: 1;
73
+ }
74
+
75
+ .eye-icon img {
76
+ width: 20px;
77
+ height: 20px;
78
+ filter: var(--icon-color);
79
+
80
+ }
81
+
82
+ .signup-btn button {
83
+ background: var(--btn-bg);
84
+ color: var(--btn-text-color);
85
+ padding: 16px;
86
+ border: none;
87
+ border-radius: 6px;
88
+ cursor: pointer;
89
+ font-size: 16px;
90
+ width: 100%;
91
+ transition: background 0.3s ease, transform 0.2s ease;
92
+ }
93
+
94
+ .signup-btn button:hover {
95
+ background: var(--btn-bg-hover);
96
+ transform: scale(1.01);
97
+ }
98
+
99
+ .error-message {
100
+ color: var(--error-message);
101
+ font-size: 14px;
102
+ text-align: center;
103
+ margin-top: 10px;
104
+ text-align: left;
105
+ }
106
+
107
+ .login-link {
108
+ margin: 20px;
109
+ font-size: 14px;
110
+ text-align: center;
111
+ }
112
+
113
+ .login-link a {
114
+ color: var(--highlight-color);
115
+ text-decoration: none;
116
+ }
117
+
118
+ .login-link a:hover {
119
+ text-decoration: underline;
120
+ }
121
+
122
+ .spinner {
123
+ border: 3px solid var(--spinner-border);
124
+ border-top: 3px solid var(--theme-color);
125
+ border-radius: 50%;
126
+ width: 14px;
127
+ height: 14px;
128
+ animation: spin 1s linear infinite;
129
+ display: none;
130
+ margin-left: 5px;
131
+ }
132
+
133
+ @keyframes spin {
134
+ 0% {
135
+ transform: rotate(0deg);
136
+ }
137
+
138
+ 100% {
139
+ transform: rotate(360deg);
140
+ }
141
+ }
chat/static/chat/favicon_io/favicon.ico ADDED
chat/static/chat/favicon_io/site.webmanifest ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "Devnoms",
3
+ "name": "Devnoms",
4
+ "description": "A powerful chat application for seamless communication.",
5
+ "start_url": "/user/",
6
+ "display": "standalone",
7
+ "background_color": "#000000",
8
+ "theme_color": "#282a2d",
9
+ "icons": [
10
+ {
11
+ "src": "https://pdf813.netlify.app/chat/favicon_io/android-chrome-192x192.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png"
14
+ },
15
+ {
16
+ "src": "https://pdf813.netlify.app/chat/favicon_io/android-chrome-512x512.png",
17
+ "sizes": "512x512",
18
+ "type": "image/png"
19
+ },
20
+ {
21
+ "src": "https://pdf813.netlify.app/chat/favicon_io/apple-touch-icon.png",
22
+ "sizes": "180x180",
23
+ "type": "image/png"
24
+ },
25
+ {
26
+ "src": "https://pdf813.netlify.app/chat/favicon_io/favicon-16x16.png",
27
+ "sizes": "16x16",
28
+ "type": "image/png"
29
+ },
30
+ {
31
+ "src": "https://pdf813.netlify.app/chat/favicon_io/favicon-32x32.png",
32
+ "sizes": "32x32",
33
+ "type": "image/png"
34
+ }
35
+ ],
36
+ "splash_pages": [
37
+ {
38
+ "src": "https://pdf813.netlify.app/chat/img/logo_750x750.png",
39
+ "sizes": "512x512",
40
+ "type": "image/png"
41
+ }
42
+ ],
43
+ "scope": "/",
44
+ "orientation": "portrait",
45
+ "prefer_related_applications": false,
46
+ "related_applications": [],
47
+ "iarc_rating_id": "AAA-0000-0000"
48
+ }
49
+
chat/static/chat/js/base.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ document.addEventListener('DOMContentLoaded', function () {
3
+ document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`);
4
+ });
5
+
6
+ window.addEventListener('resize', function () {
7
+ document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`);
8
+ });
9
+
10
+ // Back button functionality
11
+ function goBack() {
12
+ window.history.back();
13
+ }
14
+
15
+ // Toggle menu for three dots (Edit Profile, Settings, Logout)
16
+ function toggleMenu() {
17
+ const dropdown = document.getElementById("dropdown");
18
+ dropdown.classList.toggle("show");
19
+ }
20
+
21
+ // Close dropdown menu when clicking outside
22
+ document.addEventListener("click", (event) => {
23
+ const dropdown = document.getElementById("dropdown");
24
+ if (!dropdown.contains(event.target) && !event.target.closest(".menu")) {
25
+ dropdown.classList.remove("show");
26
+ }
27
+ });
28
+ // Toggle dark mode
29
+ const darkModeToggle = document.querySelector(".dark-mode-toggle");
30
+ darkModeToggle.addEventListener("click", () => {
31
+ const theme = document.documentElement.getAttribute("data-theme");
32
+ if (theme === "light") {
33
+ darkModeToggle.innerHTML = "Light Mode";
34
+ document.documentElement.setAttribute("data-theme", "dark");
35
+ } else {
36
+ darkModeToggle.innerHTML = "Dark Mode";
37
+ document.documentElement.setAttribute("data-theme", "light");
38
+ }
39
+ });
chat/static/chat/js/chat.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const chatArea = document.getElementById("chat-area");
2
+ const messageInput = document.getElementById("message-input");
3
+ const sendButton = document.getElementById("send-button");
4
+
5
+ // Automatically scroll to the bottom of the chat area
6
+ const scrollToBottom = () => {
7
+ chatArea.scrollTop = chatArea.scrollHeight;
8
+ };
9
+
10
+ // Add a new message to the chat area
11
+ const addMessage = (message, type) => {
12
+ const messageDiv = document.createElement("div");
13
+ messageDiv.classList.add("chat-message", type);
14
+
15
+ const textDiv = document.createElement("div");
16
+ textDiv.classList.add("message-text");
17
+ textDiv.innerHTML = message.replace(/\n/g, "<br>"); // Replace line breaks with <br>
18
+
19
+ const timestampDiv = document.createElement("div");
20
+ timestampDiv.classList.add("timestamp");
21
+ const currentTime = new Date();
22
+ timestampDiv.textContent = currentTime.toLocaleTimeString([], {
23
+ hour: "2-digit",
24
+ minute: "2-digit",
25
+ });
26
+
27
+ messageDiv.appendChild(textDiv);
28
+ messageDiv.appendChild(timestampDiv);
29
+ chatArea.appendChild(messageDiv);
30
+
31
+ scrollToBottom();
32
+ };
33
+
34
+ // Send a message when the send button is clicked
35
+ sendButton.addEventListener("click", () => {
36
+ const message = messageInput.value.trim();
37
+ if (message) {
38
+ addMessage(message, "sent");
39
+ messageInput.value = "";
40
+ messageInput.style.height = "46px";
41
+
42
+ // Keep the focus on the textarea
43
+ messageInput.focus();
44
+ }
45
+ });
46
+
47
+ // Auto-expand the textarea as the user types
48
+ messageInput.addEventListener("input", () => {
49
+ messageInput.style.height = "auto";
50
+ messageInput.style.height = `${messageInput.scrollHeight - 4}px`;
51
+ });
52
+
53
+ // Allow multi-line input using Enter key
54
+ // messageInput.addEventListener('keydown', (event) => {
55
+ // if (event.key === 'Enter' && event.shiftKey) {
56
+ // event.preventDefault();
57
+ // sendButton.click(); // Trigger the send button
58
+ // }
59
+ // });
60
+ scrollToBottom();
chat/static/chat/js/home.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const navButtons = document.querySelectorAll(".bottom-nav button");
2
+ const slider = document.createElement("div");
3
+ slider.className = "slider";
4
+ document.querySelector(".bottom-nav").appendChild(slider);
5
+
6
+ // Initialize the slider position to the first active button
7
+ function setSliderPosition(button) {
8
+ const buttonRect = button.getBoundingClientRect();
9
+ const navRect = document.querySelector(".bottom-nav").getBoundingClientRect();
10
+ const leftPosition =
11
+ buttonRect.left -
12
+ navRect.left +
13
+ buttonRect.width / 2 -
14
+ slider.offsetWidth / 2;
15
+ slider.style.left = `${leftPosition}px`;
16
+ }
17
+
18
+ navButtons.forEach((button) => {
19
+ button.addEventListener("click", () => {
20
+ navButtons.forEach((btn) => btn.classList.remove("active"));
21
+ button.classList.add("active");
22
+ setSliderPosition(button);
23
+ });
24
+ });
25
+
26
+ // Set initial position of the slider
27
+ setSliderPosition(document.querySelector(".bottom-nav .active"));
chat/static/chat/js/login.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function togglePasswordVisibility() {
2
+ const passwordField = document.getElementById('password');
3
+ const toggleIcon = document.getElementById('toggle-password');
4
+
5
+ if (passwordField.type === 'password') {
6
+ passwordField.type = 'text';
7
+ toggleIcon.innerHTML = '<img src="https://pdf813.netlify.app/chat/img/icon/icons8-closed-eye-50.png" alt="Hide Password" />';
8
+
9
+ } else {
10
+ passwordField.type = 'password';
11
+ toggleIcon.innerHTML = '<img src="https://pdf813.netlify.app/chat/img/icon/icons8-eye-48.png" alt="Show Password" />';
12
+ }
13
+
14
+ }
15
+
16
+ // Form validation and error handling
17
+ const form = document.getElementById('login-form');
18
+ const emailInput = document.getElementById('email');
19
+ const passwordInput = document.getElementById('password');
20
+ const emailError = document.getElementById('email-error');
21
+ const passwordError = document.getElementById('password-error');
22
+ const loginBtn = document.getElementById('login-btn');
23
+
24
+ form.addEventListener('submit', function (event) {
25
+ event.preventDefault();
26
+ let hasError = false;
27
+
28
+ // Clear previous error messages
29
+ emailError.textContent = '';
30
+ passwordError.textContent = '';
31
+
32
+ // Validate email
33
+ if (emailInput.value.trim() === '') {
34
+ emailError.textContent = 'Please fill in the email field';
35
+ hasError = true;
36
+ } else if (!validateEmail(emailInput.value)) {
37
+ emailError.textContent = 'Email is not valid';
38
+ hasError = true;
39
+ }
40
+
41
+ // Validate password
42
+ if (passwordInput.value.trim() === '') {
43
+ passwordError.textContent = 'Please fill in the password field';
44
+ hasError = true;
45
+ }
46
+
47
+ if (hasError) return;
48
+
49
+ // Show spinner and disable button
50
+ loginBtn.disabled = true;
51
+ loginBtn.innerHTML = 'Logging in... <div class="spinner" id="spinner"></div>';
52
+ const spinner = document.getElementById('spinner');
53
+ spinner.style.display = 'inline-block';
54
+
55
+ // Simulate an API request for login (replace this with your actual login API call)
56
+ setTimeout(function () {
57
+ // Simulate a response: if login fails
58
+ let loginSuccess = false; // Change this to simulate success/failure
59
+ if (!loginSuccess) {
60
+ emailError.textContent = 'Email not found or incorrect password';
61
+ passwordError.textContent = 'Please try again';
62
+ spinner.style.display = 'none';
63
+ loginBtn.disabled = false;
64
+ loginBtn.innerText = "Login";
65
+ } else {
66
+ window.location.href = '/dashboard'; // Redirect to dashboard after successful login
67
+ }
68
+ }, 3000); // Shortened timeout for testing purposes
69
+ });
70
+
71
+ function validateEmail(email) {
72
+ const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
73
+ return regex.test(email);
74
+ }
chat/static/chat/js/profile.js ADDED
@@ -0,0 +1 @@
 
 
1
+ console.log("profile");
chat/static/chat/js/search.js ADDED
@@ -0,0 +1 @@
 
 
1
+ console.log("search");
chat/static/chat/js/settings.js ADDED
@@ -0,0 +1 @@
 
 
1
+ console.log("settings");
chat/static/chat/js/signup.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function togglePasswordVisibility() {
2
+ const passwordField = document.getElementById('password');
3
+ const toggleIcon = document.getElementById('toggle-password');
4
+
5
+ if (passwordField.type === 'password') {
6
+ passwordField.type = 'text';
7
+ toggleIcon.innerHTML = '<img src="https://pdf813.netlify.app/chat/img/icon/icons8-closed-eye-50.png" alt="Hide Password" />';
8
+
9
+ } else {
10
+ passwordField.type = 'password';
11
+ toggleIcon.innerHTML = '<img src="https://pdf813.netlify.app/chat/img/icon/icons8-eye-48.png" alt="Show Password" />';
12
+ }
13
+
14
+ }
15
+
16
+ // Form validation and error handling
17
+ const form = document.getElementById('signup-form');
18
+ const nameInput = document.getElementById('name');
19
+ const emailInput = document.getElementById('email');
20
+ const passwordInput = document.getElementById('password');
21
+ const confirmPasswordInput = document.getElementById('confirm-password');
22
+ const nameError = document.getElementById('name-error');
23
+ const emailError = document.getElementById('email-error');
24
+ const passwordError = document.getElementById('password-error');
25
+ const confirmPasswordError = document.getElementById('confirm-password-error');
26
+ const signupBtn = document.getElementById('signup-btn');
27
+
28
+ form.addEventListener('submit', function (event) {
29
+ event.preventDefault();
30
+ let hasError = false;
31
+
32
+ // Clear previous error messages
33
+ nameError.textContent = '';
34
+ emailError.textContent = '';
35
+ passwordError.textContent = '';
36
+ confirmPasswordError.textContent = '';
37
+
38
+ // Validate name
39
+ if (nameInput.value.trim() === '') {
40
+ nameError.textContent = 'Please fill in the username field';
41
+ hasError = true;
42
+ }
43
+
44
+ // Validate email
45
+ if (emailInput.value.trim() === '') {
46
+ emailError.textContent = 'Please fill in the email field';
47
+ hasError = true;
48
+ } else if (!validateEmail(emailInput.value)) {
49
+ emailError.textContent = 'Email is not valid';
50
+ hasError = true;
51
+ }
52
+
53
+ // Validate password
54
+ if (passwordInput.value.trim() === '') {
55
+ passwordError.textContent = 'Please fill in the password field';
56
+ hasError = true;
57
+ }
58
+
59
+ // Validate confirm password
60
+ if (confirmPasswordInput.value !== passwordInput.value) {
61
+ confirmPasswordError.textContent = 'Passwords do not match';
62
+ hasError = true;
63
+ }
64
+
65
+ if (hasError) return;
66
+
67
+ // Show spinner and disable button
68
+ signupBtn.disabled = true;
69
+ signupBtn.innerHTML = 'Signing Up... <div class="spinner" id="spinner"></div>';
70
+ const spinner = document.getElementById('spinner');
71
+ spinner.style.display = 'inline-block';
72
+
73
+ // Simulate an API request for signup (replace this with your actual signup API call)
74
+ setTimeout(function () {
75
+ // Simulate a response: if signup fails
76
+ let signupSuccess = false; // Change this to simulate success/failure
77
+ if (!signupSuccess) {
78
+ emailError.textContent = 'Email is already taken';
79
+ spinner.style.display = 'none';
80
+ signupBtn.disabled = false;
81
+ signupBtn.innerText = "Sign Up";
82
+ } else {
83
+ window.location.href = '/login'; // Redirect to login after successful signup
84
+ }
85
+ }, 3000); // Shortened timeout for testing purposes
86
+ });
87
+
88
+ function validateEmail(email) {
89
+ const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2, 6}$/;
90
+ return regex.test(email);
91
+ }
chat/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
chat/views/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .chat_view import *
2
+ from .friends_view import *
3
+ from .home_view import *
4
+ from .user_view import *
5
+ from .password_view import *
6
+ from .test_view import *
chat/views/chat_view.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.shortcuts import render, redirect
2
+ from django.contrib.auth.models import User
3
+ from django.contrib.auth.decorators import login_required
4
+ from chat.models import UserRelation, Messages
5
+ from django.http.response import JsonResponse
6
+ from django.contrib import messages as django_messages
7
+ from django.db.models import Q # Import Q
8
+
9
+
10
+ @login_required(login_url="login")
11
+ def chat(request, username):
12
+ usersen = request.user
13
+ try:
14
+ # Get the friend and the relation in one go
15
+ friend = User.objects.get(username=username)
16
+ relation = UserRelation.objects.get(user=usersen, friend=friend, accepted=True)
17
+ except (User.DoesNotExist, UserRelation.DoesNotExist):
18
+ # If user or relation doesn't exist, show error message and redirect
19
+ django_messages.error(
20
+ request, "User or relation not found. You cannot chat with this user."
21
+ )
22
+ return redirect("home")
23
+
24
+ # Retrieve messages in one query, sorted by timestamp
25
+ messages = Messages.objects.filter(
26
+ (Q(sender_name=usersen) & Q(receiver_name=friend))
27
+ | (Q(sender_name=friend) & Q(receiver_name=usersen))
28
+ ).order_by("timestamp")
29
+
30
+ # Handle GET method to render the chat
31
+ if request.method == "GET":
32
+ return render(
33
+ request,
34
+ "chat.html",
35
+ {
36
+ "relation_key": relation.relation_key,
37
+ "messages": messages,
38
+ "curr_user": usersen,
39
+ "friend": friend,
40
+ },
41
+ )
42
+
43
+ # If the method is not GET, return an error response
44
+ return JsonResponse({"error": "Invalid request method"}, status=405)
45
+
46
+
47
+ # @login_required(login_url="login")
48
+ # @csrf_exempt
49
+ # def message_list(request, sender=None, receiver=None):
50
+ # if request.method == "GET":
51
+ # messages = Messages.objects.filter(
52
+ # sender_name=sender, receiver_name=receiver, seen=False
53
+ # )
54
+ # serializer = MessageSerializer(
55
+ # messages, many=True, context={"request": request}
56
+ # )
57
+ # for message in messages:
58
+ # message.seen = True
59
+ # message.save()
60
+ # return JsonResponse(serializer.data, safe=False)
61
+
62
+ # elif request.method == "POST":
63
+ # data = JSONParser().parse(request)
64
+ # serializer = MessageSerializer(data=data)
65
+ # if serializer.is_valid():
66
+ # serializer.save()
67
+ # return JsonResponse(serializer.data, status=201)
68
+ # return JsonResponse(serializer.errors, status=400)
chat/views/friends_view.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.shortcuts import render, redirect
2
+ from django.contrib.auth.models import User
3
+ from django.contrib.auth.decorators import login_required
4
+ from chat.models import UserRelation
5
+ from django.contrib import messages
6
+ from django.http import HttpResponseRedirect
7
+ from django.urls import reverse
8
+
9
+ @login_required(login_url="login")
10
+ def delete_friend(request):
11
+ if request.method == "POST":
12
+ username = request.POST.get("username")
13
+ requestDelete = request.POST.get("requestDelete")
14
+ user = request.user
15
+ friend = User.objects.get(username=username)
16
+ try:
17
+ # print("starts")
18
+ exists = UserRelation.objects.filter(user=user, friend=friend).exists()
19
+ if requestDelete == "True":
20
+ reverse_exists = UserRelation.objects.filter(
21
+ user=friend, friend=user
22
+ ).exists()
23
+ if reverse_exists:
24
+ return HttpResponseRedirect(
25
+ request.META.get("HTTP_REFERER", reverse("home"))
26
+ )
27
+
28
+ # print("sts")
29
+ if exists:
30
+ pass
31
+ else:
32
+ return HttpResponseRedirect(
33
+ request.META.get("HTTP_REFERER", reverse("home"))
34
+ )
35
+ user_relation = UserRelation.objects.get(user=user, friend=friend)
36
+ user_relation.delete()
37
+
38
+ user_relation_reverse = UserRelation.objects.get(user=friend, friend=user)
39
+ user_relation_reverse.delete()
40
+ messages.success(request, "Friend deleted successfully.")
41
+
42
+ except UserRelation.DoesNotExist:
43
+ messages.success(request, "Request deleted successfully.")
44
+ pass
45
+ return redirect("home")
46
+ else:
47
+ return redirect("home")
48
+
49
+
50
+ @login_required(login_url="login")
51
+ def accept_request(request):
52
+ if request.method == "POST":
53
+ username = request.POST.get("username")
54
+ user = request.user
55
+ friend = User.objects.get(username=username)
56
+ accepted = True
57
+
58
+ exists = UserRelation.objects.filter(user=user, friend=friend).exists()
59
+ reverse_exists = UserRelation.objects.filter(user=friend, friend=user).exists()
60
+
61
+ # print("sts")
62
+ if exists:
63
+ return HttpResponseRedirect(
64
+ request.META.get("HTTP_REFERER", reverse("home"))
65
+ )
66
+ if not reverse_exists:
67
+ return HttpResponseRedirect(
68
+ request.META.get("HTTP_REFERER", reverse("home"))
69
+ )
70
+ relation_key = username + "_" + user.username
71
+ user_relation = UserRelation(
72
+ user=user, friend=friend, accepted=accepted, relation_key=relation_key
73
+ )
74
+ user_relation.save()
75
+
76
+ user_relation_revrse = UserRelation.objects.get(user=friend, friend=user)
77
+ user_relation_revrse.accepted = True
78
+ user_relation_revrse.relation_key = relation_key
79
+ user_relation_revrse.save()
80
+ messages.success(request, "Friend Added successfully.")
81
+
82
+ return redirect("home")
83
+ else:
84
+ return redirect("home")
85
+
86
+
87
+ @login_required(login_url="login")
88
+ def add_friend(request):
89
+ if request.method == "POST":
90
+ username = request.POST.get("username")
91
+ user = request.user
92
+ friend = User.objects.get(username=username)
93
+ accepted = False
94
+ # print("starts")
95
+ exists = UserRelation.objects.filter(user=user, friend=friend).exists()
96
+ reverse_exists = UserRelation.objects.filter(user=friend, friend=user).exists()
97
+ # print("sts")
98
+ if exists or reverse_exists:
99
+ # print("star")
100
+ return HttpResponseRedirect(
101
+ request.META.get("HTTP_REFERER", reverse("home"))
102
+ )
103
+ user_relation = UserRelation(user=user, friend=friend, accepted=accepted)
104
+ user_relation.save()
105
+ messages.success(request, "Request sended successfully.")
106
+
107
+ return redirect("home")
108
+ else:
109
+ return redirect("home")
110
+
111
+
112
+ @login_required(login_url="login")
113
+ def search(request):
114
+ if request.method == "GET":
115
+ query = request.GET.get("q", "")
116
+ if query:
117
+ users = User.objects.filter(username__icontains=query)
118
+ if users:
119
+ return render(
120
+ request,
121
+ "search.html",
122
+ {"query": query, "users": users, "user": request.user.username},
123
+ )
124
+ else:
125
+ not_found_message = f'No users found for "{query}"'
126
+ return render(
127
+ request,
128
+ "search.html",
129
+ {
130
+ "query": query,
131
+ "not_found_message": not_found_message,
132
+ },
133
+ )
134
+
135
+ return render(request, "search.html", {"user": request.user.username})
136
+
chat/views/home_view.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.shortcuts import render, redirect
2
+ from django.contrib.auth.models import User
3
+ from django.contrib.auth.decorators import login_required
4
+ from chat.models import UserRelation
5
+ from django.contrib import messages
6
+
7
+
8
+ @login_required(login_url="login")
9
+ def userprofile(request, username):
10
+ if username == request.user.username:
11
+ return redirect("/")
12
+ friend_dict = {}
13
+ request_dict = {}
14
+ friend_dict["accepted"] = False
15
+ request_dict["accepted"] = False
16
+ friend_dict["name"] = ""
17
+ send_request = False
18
+ not_accepted = False
19
+ me_not_accepted = False
20
+ is_friend = False
21
+ try:
22
+ user = User.objects.get(username=username)
23
+ friends_data = UserRelation.objects.all()
24
+ for obj in friends_data:
25
+ if obj.user.username == request.user.username:
26
+ if obj.friend.username == username:
27
+ friend_dict = {
28
+ "name": obj.friend.username,
29
+ "accepted": obj.accepted,
30
+ }
31
+ for obj in friends_data:
32
+ if obj.friend.username == request.user.username:
33
+ if obj.user.username == username:
34
+ if obj.accepted:
35
+ me_not_accepted = False
36
+ else:
37
+ me_not_accepted = True
38
+
39
+ except User.DoesNotExist:
40
+ messages.error(request, "User does not exist.")
41
+ return render(request, "friend.html")
42
+
43
+ if friend_dict["name"] == "":
44
+ if me_not_accepted == True:
45
+ pass
46
+ # print("me not accepted")
47
+ else:
48
+ # print("not a friend")
49
+ send_request = True
50
+
51
+ elif friend_dict["accepted"] == False:
52
+ # print("not_accepted")
53
+ not_accepted = True
54
+
55
+ else:
56
+ # print("friend")
57
+ is_friend = True
58
+ # print("send_request = ", send_request)
59
+ # print("not_accepted = ", not_accepted)
60
+ # print("me_not_accepted = ", me_not_accepted)
61
+ # print("is_friend = ", is_friend)
62
+ # You can now access user details, such as username, email, etc.
63
+ user_details = {
64
+ "username": user.username,
65
+ "email": user.email,
66
+ "send_request": send_request,
67
+ "not_accepted": not_accepted,
68
+ "is_friend": is_friend,
69
+ "me_not_accepted": me_not_accepted,
70
+ }
71
+
72
+ return render(request, "friend.html", {"user_details": user_details})
73
+
74
+
75
+ from django.shortcuts import render, redirect
76
+ from django.contrib.auth.models import User
77
+ from django.contrib.auth.decorators import login_required
78
+ from chat.models import UserRelation
79
+ from django.contrib import messages
80
+ @login_required(login_url="login")
81
+ def HomePage(request):
82
+ friends_data = UserRelation.objects.all()
83
+ friends_list = []
84
+ for obj in friends_data:
85
+ if obj.user.username == request.user.username:
86
+ friend_dict = {"username": obj.friend.username, "accepted": obj.accepted}
87
+ friends_list.append(friend_dict)
88
+
89
+ request_list = []
90
+ for obj in friends_data:
91
+ if obj.friend.username == request.user.username:
92
+ if not obj.accepted:
93
+ request_dict = {"username": obj.user.username}
94
+ request_list.append(request_dict)
95
+
96
+ data = {
97
+ "email": request.user.email,
98
+ "username": request.user.username,
99
+ "friends": friends_list,
100
+ "requests": request_list,
101
+ }
102
+ return render(
103
+ request,
104
+ "home.html",
105
+ {
106
+ "data": data,
107
+ },
108
+ )
109
+
110
+
chat/views/password_view.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib.auth.models import User
2
+ from django.contrib.auth import views as auth_views
3
+
4
+ class CustomPasswordResetView(auth_views.PasswordResetView):
5
+ template_name = './password/password_reset_form.html'
6
+ email_template_name = './password/password_reset_email.html'
7
+
8
+ def form_valid(self, form):
9
+ email = form.cleaned_data['email']
10
+ if not User.objects.filter(email=email).exists():
11
+ form.add_error(
12
+ 'email', 'This email is not associated with any user in our system.')
13
+ return self.form_invalid(form)
14
+ return super().form_valid(form)
chat/views/test_view.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.shortcuts import render
2
+
3
+
4
+ def test(request):
5
+
6
+ return render(
7
+ request,
8
+ "./test/dev.html",
9
+ )
10
+
11
+
12
+ def test_chat(request):
13
+
14
+ return render(
15
+ request,
16
+ "./test/chat.html",
17
+ )
18
+
19
+
20
+ def test_home(request):
21
+
22
+ return render(
23
+ request,
24
+ "./test/home.html",
25
+ )
26
+
27
+
28
+ def test_login(request):
29
+
30
+ return render(
31
+ request,
32
+ "./test/login.html",
33
+ )
34
+
35
+
36
+ def test_signup(request):
37
+
38
+ return render(
39
+ request,
40
+ "./test/signup.html",
41
+ )
42
+
43
+
44
+ def test_profile(request):
45
+
46
+ return render(
47
+ request,
48
+ "./test/profile.html",
49
+ )
50
+
51
+
52
+ def test_search(request):
53
+
54
+ return render(
55
+ request,
56
+ "./test/search.html",
57
+ )
58
+
59
+
60
+ def test_settings(request):
61
+
62
+ return render(
63
+ request,
64
+ "./test/settings.html",
65
+ )
chat/views/user_view.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.shortcuts import render, redirect
2
+ from django.contrib.auth.models import User
3
+ from django.contrib.auth import authenticate, login, logout
4
+ from django.contrib.auth.decorators import login_required
5
+ from django.db.models import Q
6
+
7
+ @login_required(login_url="login")
8
+ def EditProfile(request):
9
+ success_message = None
10
+ error_message = None
11
+
12
+ if request.method == "POST":
13
+ new_email = request.POST.get("email")
14
+ new_username = request.POST.get("username")
15
+
16
+ # Check if the new username is already taken
17
+ if (
18
+ new_username != request.user.username
19
+ and User.objects.filter(username=new_username).exists()
20
+ ):
21
+ error_message = "Username already exists. Please choose a different one."
22
+ elif (
23
+ new_email != request.user.email
24
+ and User.objects.filter(email=new_email).exists()
25
+ ):
26
+ error_message = "Email address already associated with another account. Please choose a different one."
27
+ else:
28
+ # Update email and username
29
+ # print(request.user.id)
30
+ request.user.email = new_email
31
+ request.user.username = new_username
32
+ request.user.save()
33
+ success_message = "Profile updated successfully."
34
+
35
+ # Pre-fill the form with the user's existing data
36
+ initial_data = {
37
+ "email": request.user.email,
38
+ "username": request.user.username,
39
+ }
40
+
41
+ return render(
42
+ request,
43
+ "edit.html",
44
+ {
45
+ "initial_data": initial_data,
46
+ "success_message": success_message,
47
+ "error_message": error_message,
48
+ },
49
+ )
50
+
51
+
52
+ def SignupPage(request):
53
+ if request.user.is_authenticated:
54
+ return redirect("home")
55
+ error_message = "" # Initialize error_message as None
56
+
57
+ if request.method == "POST":
58
+ uname = request.POST.get("username")
59
+ email = request.POST.get("email")
60
+ pass1 = request.POST.get("password1")
61
+ pass2 = request.POST.get("password2")
62
+
63
+ data = {
64
+ "username": uname,
65
+ "useremail": email,
66
+ }
67
+
68
+ # Check if a user with the same email or username already exists
69
+ if User.objects.filter(username=uname).exists():
70
+ error_message = "A user with the same username already exists."
71
+ return render(
72
+ request,
73
+ "signup.html",
74
+ {"error_message": error_message, "userdata": data},
75
+ )
76
+
77
+ elif User.objects.filter(email=email).exists():
78
+ error_message = "A user with the same email already exists."
79
+ return render(
80
+ request,
81
+ "signup.html",
82
+ {"error_message": error_message, "userdata": data},
83
+ )
84
+
85
+ else:
86
+ # Create the user
87
+ user = User.objects.create_user(username=uname, email=email, password=pass1)
88
+ user.save()
89
+ # Log the user in after registration
90
+ login(request, user)
91
+ return redirect("home")
92
+
93
+ return render(request, "signup.html", {"error_message": error_message})
94
+
95
+
96
+ def LoginPage(request):
97
+ if request.user.is_authenticated:
98
+ return redirect("home")
99
+ error_message = ""
100
+ email_or_username = ""
101
+ if request.method == "POST":
102
+ email_or_username = request.POST.get("email_or_username")
103
+ pass1 = request.POST.get("pass")
104
+ try:
105
+ curr_user = User.objects.get(Q(email=email_or_username) | Q(username=email_or_username))
106
+ except User.DoesNotExist:
107
+ error_message = "User not found. Please check your Email/Username"
108
+ return render(
109
+ request, "login.html", {"error_message": error_message, "email": email_or_username}
110
+ )
111
+
112
+ user = authenticate(request, username=curr_user.username, password=pass1)
113
+ if user is not None:
114
+ login(request, user)
115
+ return redirect("home")
116
+ else:
117
+ if User.objects.filter(username=curr_user.username).exists():
118
+ error_message = "Incorrect password. Please try again."
119
+ else:
120
+ error_message = "Email not found. Please check your email."
121
+ return render(
122
+ request, "login.html", {"error_message": error_message, "email": email_or_username}
123
+ )
124
+
125
+
126
+ def LogoutPage(request):
127
+ logout(request)
128
+ return redirect("login")
devnoms/__init__.py ADDED
File without changes
devnoms/asgi.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from django.core.asgi import get_asgi_application
3
+ from channels.routing import ProtocolTypeRouter, URLRouter
4
+ from channels.auth import AuthMiddlewareStack
5
+ import chat.routing
6
+
7
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devnoms.settings')
8
+
9
+ application = ProtocolTypeRouter({
10
+ "http": get_asgi_application(),
11
+ "websocket": AuthMiddlewareStack(
12
+ URLRouter(
13
+ chat.routing.websocket_urlpatterns
14
+ )
15
+ ),
16
+ })
devnoms/settings.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from dotenv import load_dotenv
3
+ import os
4
+
5
+ load_dotenv()
6
+
7
+ DEVELOPMENT = "live"
8
+ # DEVELOPMENT = "local"
9
+ DEBUG = True
10
+
11
+
12
+ if DEVELOPMENT == "local":
13
+ # Development
14
+ DEBUG = True
15
+ DATABASES = {
16
+ "default": {
17
+ "ENGINE": "django.db.backends.postgresql",
18
+ "NAME": "devnoms_localdb",
19
+ "USER": os.getenv("DB_USER_LOCAL"),
20
+ "PASSWORD": os.getenv("DB_PASS_LOCAL"),
21
+ "HOST": os.getenv("DB_HOST_LOCAL"),
22
+ "PORT": "",
23
+ }
24
+ }
25
+ SITE_URL = "http://localhost:8000"
26
+
27
+
28
+ else:
29
+ # Live
30
+ # DEBUG = False
31
+ DATABASES = {
32
+ "default": {
33
+ "ENGINE": "django.db.backends.postgresql",
34
+ "NAME": os.getenv("DB_NAME_LIVE"),
35
+ "USER": os.getenv("DB_USER_LIVE"),
36
+ "PASSWORD": os.getenv("DB_PASS_LIVE"),
37
+ "HOST": os.getenv("DB_HOST_LIVE"),
38
+ "PORT": "",
39
+ "OPTIONS": {
40
+ "sslmode": "require",
41
+ },
42
+ }
43
+ }
44
+ SITE_URL = "https://devnoms.onrender.com"
45
+
46
+
47
+ BASE_DIR = Path(__file__).resolve().parent.parent
48
+
49
+
50
+ SECRET_KEY = os.getenv("SECRET_KEY")
51
+ ALLOWED_HOSTS = ["*"]
52
+
53
+
54
+ INSTALLED_APPS = [
55
+ "channels",
56
+ "django.contrib.admin",
57
+ "django.contrib.auth",
58
+ "django.contrib.contenttypes",
59
+ "django.contrib.sessions",
60
+ "django.contrib.messages",
61
+ "django.contrib.staticfiles",
62
+ "chat",
63
+ "corsheaders",
64
+ ]
65
+ ASGI_APPLICATION = "devnoms.asgi.application"
66
+
67
+ CHANNEL_LAYERS = {
68
+ "default": {
69
+ "BACKEND": "channels.layers.InMemoryChannelLayer",
70
+ }
71
+ }
72
+
73
+
74
+ MIDDLEWARE = [
75
+ "django.middleware.security.SecurityMiddleware",
76
+ "django.contrib.sessions.middleware.SessionMiddleware",
77
+ "django.middleware.common.CommonMiddleware",
78
+ "django.middleware.csrf.CsrfViewMiddleware",
79
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
80
+ "django.contrib.messages.middleware.MessageMiddleware",
81
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
82
+ "corsheaders.middleware.CorsMiddleware",
83
+ ]
84
+
85
+ ROOT_URLCONF = "devnoms.urls"
86
+
87
+ TEMPLATES = [
88
+ {
89
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
90
+ "DIRS": ["templates"],
91
+ "APP_DIRS": True,
92
+ "OPTIONS": {
93
+ "context_processors": [
94
+ "django.template.context_processors.debug",
95
+ "django.template.context_processors.request",
96
+ "django.contrib.auth.context_processors.auth",
97
+ "django.contrib.messages.context_processors.messages",
98
+ ],
99
+ },
100
+ },
101
+ ]
102
+
103
+ WSGI_APPLICATION = "devnoms.wsgi.application"
104
+
105
+ AUTH_PASSWORD_VALIDATORS = [
106
+ {
107
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
108
+ },
109
+ {
110
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
111
+ },
112
+ {
113
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
114
+ },
115
+ {
116
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
117
+ },
118
+ ]
119
+
120
+
121
+ LANGUAGE_CODE = "en-us"
122
+
123
+ # TIME_ZONE = "UTC"
124
+
125
+ TIME_ZONE = "Asia/Kolkata"
126
+
127
+ USE_I18N = True
128
+
129
+ USE_L10N = True
130
+
131
+ USE_TZ = True
132
+
133
+ LOGIN_URL = "login"
134
+
135
+
136
+ STATIC_URL = "/static/"
137
+
138
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
139
+
140
+ STATIC_ROOT = os.path.join(BASE_DIR, "static")
141
+
142
+ CORS_ALLOWED_ORIGINS = [
143
+ "http://127.0.0.1:5500",
144
+ "http://localhost:3000",
145
+ "https://devnoms.onrender.com",
146
+ "https://django-chat-application.onrender.com",
147
+ "https://coders813-django-chat-application.hf.space"
148
+ ]
149
+ CORS_ALLOW_ALL_ORIGINS = True
150
+ CSRF_TRUSTED_ORIGINS = [
151
+ "https://devnoms.onrender.com",
152
+ "https://django-chat-application.onrender.com",
153
+ "https://coders813-django-chat-application.hf.space"
154
+ ]
155
+
156
+ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
157
+ EMAIL_HOST = "smtp.gmail.com"
158
+ EMAIL_PORT = 587
159
+ EMAIL_USE_TLS = True
160
+ EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
161
+ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
devnoms/urls.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from chat.views import *
3
+ from django.urls import path
4
+ from django.conf import settings
5
+ from django.contrib.staticfiles.urls import static
6
+ from django.contrib.auth import views as auth_views
7
+
8
+ urlpatterns = [
9
+ path("admin/", admin.site.urls),
10
+ path("", LoginPage, name="login"),
11
+ path("signup/", SignupPage, name="signup"),
12
+ path("logout/", LogoutPage, name="logout"),
13
+ path("user/", HomePage, name="home"),
14
+ path("edit/", EditProfile, name="edit"),
15
+ path("user/<str:username>/", userprofile, name="username"),
16
+ path("add_friend/", add_friend, name="add_friend"),
17
+ path("accept_request/", accept_request, name="accept_request"),
18
+ path("delete_friend/", delete_friend, name="delete_friend"),
19
+ path("search/", search, name="search"),
20
+ # testing
21
+ path("test/", test, name="test"),
22
+ path("test/chat/", test_chat, name="test_chat"),
23
+ path("test/home/", test_home, name="test_home"),
24
+ path("test/login/", test_login, name="test_login"),
25
+ path("test/signup/", test_signup, name="test_signup"),
26
+ path("test/search/", test_search, name="test_search"),
27
+ path("test/profile/", test_profile, name="test_profile"),
28
+ path("test/settings/", test_settings, name="test_settings"),
29
+ # re_path(r"^.*/$", RedirectView.as_view(pattern_name="login", permanent=False)),
30
+ path("chat/<str:username>/", chat, name="chat"),
31
+ path(
32
+ "password-reset/",
33
+ CustomPasswordResetView.as_view(),
34
+ name="password_reset",
35
+ ),
36
+ path(
37
+ "password-reset/done/",
38
+ auth_views.PasswordResetDoneView.as_view(
39
+ template_name="./password/password_reset_done.html"
40
+ ),
41
+ name="password_reset_done",
42
+ ),
43
+ path(
44
+ "reset/<uidb64>/<token>/",
45
+ auth_views.PasswordResetConfirmView.as_view(
46
+ template_name="./password/password_reset_confirm.html"
47
+ ),
48
+ name="password_reset_confirm",
49
+ ),
50
+ path(
51
+ "reset/done/",
52
+ auth_views.PasswordResetCompleteView.as_view(
53
+ template_name="./password/password_reset_complete.html"
54
+ ),
55
+ name="password_reset_complete",
56
+ ),
57
+ ]
58
+
59
+
60
+ if settings.DEBUG:
61
+ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
devnoms/wsgi.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WSGI config for devnoms project.
3
+
4
+ It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.wsgi import get_wsgi_application
13
+
14
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devnoms.settings')
15
+
16
+ application = get_wsgi_application()
manage.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Django's command-line utility for administrative tasks."""
3
+ import os
4
+ import sys
5
+
6
+
7
+ def main():
8
+ """Run administrative tasks."""
9
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devnoms.settings')
10
+ try:
11
+ from django.core.management import execute_from_command_line
12
+ except ImportError as exc:
13
+ raise ImportError(
14
+ "Couldn't import Django. Are you sure it's installed and "
15
+ "available on your PYTHONPATH environment variable? Did you "
16
+ "forget to activate a virtual environment?"
17
+ ) from exc
18
+ execute_from_command_line(sys.argv)
19
+
20
+
21
+ if __name__ == '__main__':
22
+ main()
requirements.txt ADDED
Binary file (1.21 kB). View file
 
static/admin/css/autocomplete.css ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ select.admin-autocomplete {
2
+ width: 20em;
3
+ }
4
+
5
+ .select2-container--admin-autocomplete.select2-container {
6
+ min-height: 30px;
7
+ }
8
+
9
+ .select2-container--admin-autocomplete .select2-selection--single,
10
+ .select2-container--admin-autocomplete .select2-selection--multiple {
11
+ min-height: 30px;
12
+ padding: 0;
13
+ }
14
+
15
+ .select2-container--admin-autocomplete.select2-container--focus .select2-selection,
16
+ .select2-container--admin-autocomplete.select2-container--open .select2-selection {
17
+ border-color: var(--body-quiet-color);
18
+ min-height: 30px;
19
+ }
20
+
21
+ .select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
22
+ .select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
23
+ padding: 0;
24
+ }
25
+
26
+ .select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
27
+ .select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
28
+ padding: 0;
29
+ }
30
+
31
+ .select2-container--admin-autocomplete .select2-selection--single {
32
+ background-color: var(--body-bg);
33
+ border: 1px solid var(--border-color);
34
+ border-radius: 4px;
35
+ }
36
+
37
+ .select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
38
+ color: var(--body-fg);
39
+ line-height: 30px;
40
+ }
41
+
42
+ .select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
43
+ cursor: pointer;
44
+ float: right;
45
+ font-weight: bold;
46
+ }
47
+
48
+ .select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
49
+ color: var(--body-quiet-color);
50
+ }
51
+
52
+ .select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
53
+ height: 26px;
54
+ position: absolute;
55
+ top: 1px;
56
+ right: 1px;
57
+ width: 20px;
58
+ }
59
+
60
+ .select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
61
+ border-color: #888 transparent transparent transparent;
62
+ border-style: solid;
63
+ border-width: 5px 4px 0 4px;
64
+ height: 0;
65
+ left: 50%;
66
+ margin-left: -4px;
67
+ margin-top: -2px;
68
+ position: absolute;
69
+ top: 50%;
70
+ width: 0;
71
+ }
72
+
73
+ .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
74
+ float: left;
75
+ }
76
+
77
+ .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
78
+ left: 1px;
79
+ right: auto;
80
+ }
81
+
82
+ .select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
83
+ background-color: var(--darkened-bg);
84
+ cursor: default;
85
+ }
86
+
87
+ .select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
88
+ display: none;
89
+ }
90
+
91
+ .select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
92
+ border-color: transparent transparent #888 transparent;
93
+ border-width: 0 4px 5px 4px;
94
+ }
95
+
96
+ .select2-container--admin-autocomplete .select2-selection--multiple {
97
+ background-color: var(--body-bg);
98
+ border: 1px solid var(--border-color);
99
+ border-radius: 4px;
100
+ cursor: text;
101
+ }
102
+
103
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
104
+ box-sizing: border-box;
105
+ list-style: none;
106
+ margin: 0;
107
+ padding: 0 10px 5px 5px;
108
+ width: 100%;
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ }
112
+
113
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
114
+ list-style: none;
115
+ }
116
+
117
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
118
+ color: var(--body-quiet-color);
119
+ margin-top: 5px;
120
+ float: left;
121
+ }
122
+
123
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
124
+ cursor: pointer;
125
+ float: right;
126
+ font-weight: bold;
127
+ margin: 5px;
128
+ position: absolute;
129
+ right: 0;
130
+ }
131
+
132
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
133
+ background-color: var(--darkened-bg);
134
+ border: 1px solid var(--border-color);
135
+ border-radius: 4px;
136
+ cursor: default;
137
+ float: left;
138
+ margin-right: 5px;
139
+ margin-top: 5px;
140
+ padding: 0 5px;
141
+ }
142
+
143
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
144
+ color: var(--body-quiet-color);
145
+ cursor: pointer;
146
+ display: inline-block;
147
+ font-weight: bold;
148
+ margin-right: 2px;
149
+ }
150
+
151
+ .select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
152
+ color: var(--body-fg);
153
+ }
154
+
155
+ .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
156
+ float: right;
157
+ }
158
+
159
+ .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
160
+ margin-left: 5px;
161
+ margin-right: auto;
162
+ }
163
+
164
+ .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
165
+ margin-left: 2px;
166
+ margin-right: auto;
167
+ }
168
+
169
+ .select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
170
+ border: solid var(--body-quiet-color) 1px;
171
+ outline: 0;
172
+ }
173
+
174
+ .select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
175
+ background-color: var(--darkened-bg);
176
+ cursor: default;
177
+ }
178
+
179
+ .select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
180
+ display: none;
181
+ }
182
+
183
+ .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
184
+ border-top-left-radius: 0;
185
+ border-top-right-radius: 0;
186
+ }
187
+
188
+ .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
189
+ border-bottom-left-radius: 0;
190
+ border-bottom-right-radius: 0;
191
+ }
192
+
193
+ .select2-container--admin-autocomplete .select2-search--dropdown {
194
+ background: var(--darkened-bg);
195
+ }
196
+
197
+ .select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
198
+ background: var(--body-bg);
199
+ color: var(--body-fg);
200
+ border: 1px solid var(--border-color);
201
+ border-radius: 4px;
202
+ }
203
+
204
+ .select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
205
+ background: transparent;
206
+ color: var(--body-fg);
207
+ border: none;
208
+ outline: 0;
209
+ box-shadow: none;
210
+ -webkit-appearance: textfield;
211
+ }
212
+
213
+ .select2-container--admin-autocomplete .select2-results > .select2-results__options {
214
+ max-height: 200px;
215
+ overflow-y: auto;
216
+ color: var(--body-fg);
217
+ background: var(--body-bg);
218
+ }
219
+
220
+ .select2-container--admin-autocomplete .select2-results__option[role=group] {
221
+ padding: 0;
222
+ }
223
+
224
+ .select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
225
+ color: var(--body-quiet-color);
226
+ }
227
+
228
+ .select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
229
+ background-color: var(--selected-bg);
230
+ color: var(--body-fg);
231
+ }
232
+
233
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option {
234
+ padding-left: 1em;
235
+ }
236
+
237
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
238
+ padding-left: 0;
239
+ }
240
+
241
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
242
+ margin-left: -1em;
243
+ padding-left: 2em;
244
+ }
245
+
246
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
247
+ margin-left: -2em;
248
+ padding-left: 3em;
249
+ }
250
+
251
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
252
+ margin-left: -3em;
253
+ padding-left: 4em;
254
+ }
255
+
256
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
257
+ margin-left: -4em;
258
+ padding-left: 5em;
259
+ }
260
+
261
+ .select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
262
+ margin-left: -5em;
263
+ padding-left: 6em;
264
+ }
265
+
266
+ .select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
267
+ background-color: var(--primary);
268
+ color: var(--primary-fg);
269
+ }
270
+
271
+ .select2-container--admin-autocomplete .select2-results__group {
272
+ cursor: default;
273
+ display: block;
274
+ padding: 6px;
275
+ }
276
+
277
+ .errors .select2-selection {
278
+ border: 1px solid var(--error-fg);
279
+ }
static/admin/css/base.css ADDED
@@ -0,0 +1,1179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ DJANGO Admin styles
3
+ */
4
+
5
+ /* VARIABLE DEFINITIONS */
6
+ html[data-theme="light"],
7
+ :root {
8
+ --primary: #79aec8;
9
+ --secondary: #417690;
10
+ --accent: #f5dd5d;
11
+ --primary-fg: #fff;
12
+
13
+ --body-fg: #333;
14
+ --body-bg: #fff;
15
+ --body-quiet-color: #666;
16
+ --body-medium-color: #444;
17
+ --body-loud-color: #000;
18
+
19
+ --header-color: #ffc;
20
+ --header-branding-color: var(--accent);
21
+ --header-bg: var(--secondary);
22
+ --header-link-color: var(--primary-fg);
23
+
24
+ --breadcrumbs-fg: #c4dce8;
25
+ --breadcrumbs-link-fg: var(--body-bg);
26
+ --breadcrumbs-bg: #264b5d;
27
+
28
+ --link-fg: #417893;
29
+ --link-hover-color: #036;
30
+ --link-selected-fg: var(--secondary);
31
+
32
+ --hairline-color: #e8e8e8;
33
+ --border-color: #ccc;
34
+
35
+ --error-fg: #ba2121;
36
+
37
+ --message-success-bg: #dfd;
38
+ --message-warning-bg: #ffc;
39
+ --message-error-bg: #ffefef;
40
+
41
+ --darkened-bg: #f8f8f8; /* A bit darker than --body-bg */
42
+ --selected-bg: #e4e4e4; /* E.g. selected table cells */
43
+ --selected-row: #ffc;
44
+
45
+ --button-fg: #fff;
46
+ --button-bg: var(--secondary);
47
+ --button-hover-bg: #205067;
48
+ --default-button-bg: #205067;
49
+ --default-button-hover-bg: var(--secondary);
50
+ --close-button-bg: #747474;
51
+ --close-button-hover-bg: #333;
52
+ --delete-button-bg: #ba2121;
53
+ --delete-button-hover-bg: #a41515;
54
+
55
+ --object-tools-fg: var(--button-fg);
56
+ --object-tools-bg: var(--close-button-bg);
57
+ --object-tools-hover-bg: var(--close-button-hover-bg);
58
+
59
+ --font-family-primary:
60
+ "Segoe UI",
61
+ system-ui,
62
+ Roboto,
63
+ "Helvetica Neue",
64
+ Arial,
65
+ sans-serif,
66
+ "Apple Color Emoji",
67
+ "Segoe UI Emoji",
68
+ "Segoe UI Symbol",
69
+ "Noto Color Emoji";
70
+ --font-family-monospace:
71
+ ui-monospace,
72
+ Menlo,
73
+ Monaco,
74
+ "Cascadia Mono",
75
+ "Segoe UI Mono",
76
+ "Roboto Mono",
77
+ "Oxygen Mono",
78
+ "Ubuntu Monospace",
79
+ "Source Code Pro",
80
+ "Fira Mono",
81
+ "Droid Sans Mono",
82
+ "Courier New",
83
+ monospace,
84
+ "Apple Color Emoji",
85
+ "Segoe UI Emoji",
86
+ "Segoe UI Symbol",
87
+ "Noto Color Emoji";
88
+
89
+ color-scheme: light;
90
+ }
91
+
92
+ html, body {
93
+ height: 100%;
94
+ }
95
+
96
+ body {
97
+ margin: 0;
98
+ padding: 0;
99
+ font-size: 0.875rem;
100
+ font-family: var(--font-family-primary);
101
+ color: var(--body-fg);
102
+ background: var(--body-bg);
103
+ }
104
+
105
+ /* LINKS */
106
+
107
+ a:link, a:visited {
108
+ color: var(--link-fg);
109
+ text-decoration: none;
110
+ transition: color 0.15s, background 0.15s;
111
+ }
112
+
113
+ a:focus, a:hover {
114
+ color: var(--link-hover-color);
115
+ }
116
+
117
+ a:focus {
118
+ text-decoration: underline;
119
+ }
120
+
121
+ a img {
122
+ border: none;
123
+ }
124
+
125
+ a.section:link, a.section:visited {
126
+ color: var(--header-link-color);
127
+ text-decoration: none;
128
+ }
129
+
130
+ a.section:focus, a.section:hover {
131
+ text-decoration: underline;
132
+ }
133
+
134
+ /* GLOBAL DEFAULTS */
135
+
136
+ p, ol, ul, dl {
137
+ margin: .2em 0 .8em 0;
138
+ }
139
+
140
+ p {
141
+ padding: 0;
142
+ line-height: 140%;
143
+ }
144
+
145
+ h1,h2,h3,h4,h5 {
146
+ font-weight: bold;
147
+ }
148
+
149
+ h1 {
150
+ margin: 0 0 20px;
151
+ font-weight: 300;
152
+ font-size: 1.25rem;
153
+ }
154
+
155
+ h2 {
156
+ font-size: 1rem;
157
+ margin: 1em 0 .5em 0;
158
+ }
159
+
160
+ h2.subhead {
161
+ font-weight: normal;
162
+ margin-top: 0;
163
+ }
164
+
165
+ h3 {
166
+ font-size: 0.875rem;
167
+ margin: .8em 0 .3em 0;
168
+ color: var(--body-medium-color);
169
+ font-weight: bold;
170
+ }
171
+
172
+ h4 {
173
+ font-size: 0.75rem;
174
+ margin: 1em 0 .8em 0;
175
+ padding-bottom: 3px;
176
+ color: var(--body-medium-color);
177
+ }
178
+
179
+ h5 {
180
+ font-size: 0.625rem;
181
+ margin: 1.5em 0 .5em 0;
182
+ color: var(--body-quiet-color);
183
+ text-transform: uppercase;
184
+ letter-spacing: 1px;
185
+ }
186
+
187
+ ul > li {
188
+ list-style-type: square;
189
+ padding: 1px 0;
190
+ }
191
+
192
+ li ul {
193
+ margin-bottom: 0;
194
+ }
195
+
196
+ li, dt, dd {
197
+ font-size: 0.8125rem;
198
+ line-height: 1.25rem;
199
+ }
200
+
201
+ dt {
202
+ font-weight: bold;
203
+ margin-top: 4px;
204
+ }
205
+
206
+ dd {
207
+ margin-left: 0;
208
+ }
209
+
210
+ form {
211
+ margin: 0;
212
+ padding: 0;
213
+ }
214
+
215
+ fieldset {
216
+ margin: 0;
217
+ min-width: 0;
218
+ padding: 0;
219
+ border: none;
220
+ border-top: 1px solid var(--hairline-color);
221
+ }
222
+
223
+ details summary {
224
+ cursor: pointer;
225
+ }
226
+
227
+ blockquote {
228
+ font-size: 0.6875rem;
229
+ color: #777;
230
+ margin-left: 2px;
231
+ padding-left: 10px;
232
+ border-left: 5px solid #ddd;
233
+ }
234
+
235
+ code, pre {
236
+ font-family: var(--font-family-monospace);
237
+ color: var(--body-quiet-color);
238
+ font-size: 0.75rem;
239
+ overflow-x: auto;
240
+ }
241
+
242
+ pre.literal-block {
243
+ margin: 10px;
244
+ background: var(--darkened-bg);
245
+ padding: 6px 8px;
246
+ }
247
+
248
+ code strong {
249
+ color: #930;
250
+ }
251
+
252
+ hr {
253
+ clear: both;
254
+ color: var(--hairline-color);
255
+ background-color: var(--hairline-color);
256
+ height: 1px;
257
+ border: none;
258
+ margin: 0;
259
+ padding: 0;
260
+ line-height: 1px;
261
+ }
262
+
263
+ /* TEXT STYLES & MODIFIERS */
264
+
265
+ .small {
266
+ font-size: 0.6875rem;
267
+ }
268
+
269
+ .mini {
270
+ font-size: 0.625rem;
271
+ }
272
+
273
+ .help, p.help, form p.help, div.help, form div.help, div.help li {
274
+ font-size: 0.6875rem;
275
+ color: var(--body-quiet-color);
276
+ }
277
+
278
+ div.help ul {
279
+ margin-bottom: 0;
280
+ }
281
+
282
+ .help-tooltip {
283
+ cursor: help;
284
+ }
285
+
286
+ p img, h1 img, h2 img, h3 img, h4 img, td img {
287
+ vertical-align: middle;
288
+ }
289
+
290
+ .quiet, a.quiet:link, a.quiet:visited {
291
+ color: var(--body-quiet-color);
292
+ font-weight: normal;
293
+ }
294
+
295
+ .clear {
296
+ clear: both;
297
+ }
298
+
299
+ .nowrap {
300
+ white-space: nowrap;
301
+ }
302
+
303
+ .hidden {
304
+ display: none !important;
305
+ }
306
+
307
+ /* TABLES */
308
+
309
+ table {
310
+ border-collapse: collapse;
311
+ border-color: var(--border-color);
312
+ }
313
+
314
+ td, th {
315
+ font-size: 0.8125rem;
316
+ line-height: 1rem;
317
+ border-bottom: 1px solid var(--hairline-color);
318
+ vertical-align: top;
319
+ padding: 8px;
320
+ }
321
+
322
+ th {
323
+ font-weight: 500;
324
+ text-align: left;
325
+ }
326
+
327
+ thead th,
328
+ tfoot td {
329
+ color: var(--body-quiet-color);
330
+ padding: 5px 10px;
331
+ font-size: 0.6875rem;
332
+ background: var(--body-bg);
333
+ border: none;
334
+ border-top: 1px solid var(--hairline-color);
335
+ border-bottom: 1px solid var(--hairline-color);
336
+ }
337
+
338
+ tfoot td {
339
+ border-bottom: none;
340
+ border-top: 1px solid var(--hairline-color);
341
+ }
342
+
343
+ thead th.required {
344
+ font-weight: bold;
345
+ }
346
+
347
+ tr.alt {
348
+ background: var(--darkened-bg);
349
+ }
350
+
351
+ tr:nth-child(odd), .row-form-errors {
352
+ background: var(--body-bg);
353
+ }
354
+
355
+ tr:nth-child(even),
356
+ tr:nth-child(even) .errorlist,
357
+ tr:nth-child(odd) + .row-form-errors,
358
+ tr:nth-child(odd) + .row-form-errors .errorlist {
359
+ background: var(--darkened-bg);
360
+ }
361
+
362
+ /* SORTABLE TABLES */
363
+
364
+ thead th {
365
+ padding: 5px 10px;
366
+ line-height: normal;
367
+ text-transform: uppercase;
368
+ background: var(--darkened-bg);
369
+ }
370
+
371
+ thead th a:link, thead th a:visited {
372
+ color: var(--body-quiet-color);
373
+ }
374
+
375
+ thead th.sorted {
376
+ background: var(--selected-bg);
377
+ }
378
+
379
+ thead th.sorted .text {
380
+ padding-right: 42px;
381
+ }
382
+
383
+ table thead th .text span {
384
+ padding: 8px 10px;
385
+ display: block;
386
+ }
387
+
388
+ table thead th .text a {
389
+ display: block;
390
+ cursor: pointer;
391
+ padding: 8px 10px;
392
+ }
393
+
394
+ table thead th .text a:focus, table thead th .text a:hover {
395
+ background: var(--selected-bg);
396
+ }
397
+
398
+ thead th.sorted a.sortremove {
399
+ visibility: hidden;
400
+ }
401
+
402
+ table thead th.sorted:hover a.sortremove {
403
+ visibility: visible;
404
+ }
405
+
406
+ table thead th.sorted .sortoptions {
407
+ display: block;
408
+ padding: 9px 5px 0 5px;
409
+ float: right;
410
+ text-align: right;
411
+ }
412
+
413
+ table thead th.sorted .sortpriority {
414
+ font-size: .8em;
415
+ min-width: 12px;
416
+ text-align: center;
417
+ vertical-align: 3px;
418
+ margin-left: 2px;
419
+ margin-right: 2px;
420
+ }
421
+
422
+ table thead th.sorted .sortoptions a {
423
+ position: relative;
424
+ width: 14px;
425
+ height: 14px;
426
+ display: inline-block;
427
+ background: url(../img/sorting-icons.svg) 0 0 no-repeat;
428
+ background-size: 14px auto;
429
+ }
430
+
431
+ table thead th.sorted .sortoptions a.sortremove {
432
+ background-position: 0 0;
433
+ }
434
+
435
+ table thead th.sorted .sortoptions a.sortremove:after {
436
+ content: '\\';
437
+ position: absolute;
438
+ top: -6px;
439
+ left: 3px;
440
+ font-weight: 200;
441
+ font-size: 1.125rem;
442
+ color: var(--body-quiet-color);
443
+ }
444
+
445
+ table thead th.sorted .sortoptions a.sortremove:focus:after,
446
+ table thead th.sorted .sortoptions a.sortremove:hover:after {
447
+ color: var(--link-fg);
448
+ }
449
+
450
+ table thead th.sorted .sortoptions a.sortremove:focus,
451
+ table thead th.sorted .sortoptions a.sortremove:hover {
452
+ background-position: 0 -14px;
453
+ }
454
+
455
+ table thead th.sorted .sortoptions a.ascending {
456
+ background-position: 0 -28px;
457
+ }
458
+
459
+ table thead th.sorted .sortoptions a.ascending:focus,
460
+ table thead th.sorted .sortoptions a.ascending:hover {
461
+ background-position: 0 -42px;
462
+ }
463
+
464
+ table thead th.sorted .sortoptions a.descending {
465
+ top: 1px;
466
+ background-position: 0 -56px;
467
+ }
468
+
469
+ table thead th.sorted .sortoptions a.descending:focus,
470
+ table thead th.sorted .sortoptions a.descending:hover {
471
+ background-position: 0 -70px;
472
+ }
473
+
474
+ /* FORM DEFAULTS */
475
+
476
+ input, textarea, select, .form-row p, form .button {
477
+ margin: 2px 0;
478
+ padding: 2px 3px;
479
+ vertical-align: middle;
480
+ font-family: var(--font-family-primary);
481
+ font-weight: normal;
482
+ font-size: 0.8125rem;
483
+ }
484
+ .form-row div.help {
485
+ padding: 2px 3px;
486
+ }
487
+
488
+ textarea {
489
+ vertical-align: top;
490
+ }
491
+
492
+ /*
493
+ Minifiers remove the default (text) "type" attribute from "input" HTML tags.
494
+ Add input:not([type]) to make the CSS stylesheet work the same.
495
+ */
496
+ input:not([type]), input[type=text], input[type=password], input[type=email],
497
+ input[type=url], input[type=number], input[type=tel], textarea, select,
498
+ .vTextField {
499
+ border: 1px solid var(--border-color);
500
+ border-radius: 4px;
501
+ padding: 5px 6px;
502
+ margin-top: 0;
503
+ color: var(--body-fg);
504
+ background-color: var(--body-bg);
505
+ }
506
+
507
+ /*
508
+ Minifiers remove the default (text) "type" attribute from "input" HTML tags.
509
+ Add input:not([type]) to make the CSS stylesheet work the same.
510
+ */
511
+ input:not([type]):focus, input[type=text]:focus, input[type=password]:focus,
512
+ input[type=email]:focus, input[type=url]:focus, input[type=number]:focus,
513
+ input[type=tel]:focus, textarea:focus, select:focus, .vTextField:focus {
514
+ border-color: var(--body-quiet-color);
515
+ }
516
+
517
+ select {
518
+ height: 1.875rem;
519
+ }
520
+
521
+ select[multiple] {
522
+ /* Allow HTML size attribute to override the height in the rule above. */
523
+ height: auto;
524
+ min-height: 150px;
525
+ }
526
+
527
+ /* FORM BUTTONS */
528
+
529
+ .button, input[type=submit], input[type=button], .submit-row input, a.button {
530
+ background: var(--button-bg);
531
+ padding: 10px 15px;
532
+ border: none;
533
+ border-radius: 4px;
534
+ color: var(--button-fg);
535
+ cursor: pointer;
536
+ transition: background 0.15s;
537
+ }
538
+
539
+ a.button {
540
+ padding: 4px 5px;
541
+ }
542
+
543
+ .button:active, input[type=submit]:active, input[type=button]:active,
544
+ .button:focus, input[type=submit]:focus, input[type=button]:focus,
545
+ .button:hover, input[type=submit]:hover, input[type=button]:hover {
546
+ background: var(--button-hover-bg);
547
+ }
548
+
549
+ .button[disabled], input[type=submit][disabled], input[type=button][disabled] {
550
+ opacity: 0.4;
551
+ }
552
+
553
+ .button.default, input[type=submit].default, .submit-row input.default {
554
+ border: none;
555
+ font-weight: 400;
556
+ background: var(--default-button-bg);
557
+ }
558
+
559
+ .button.default:active, input[type=submit].default:active,
560
+ .button.default:focus, input[type=submit].default:focus,
561
+ .button.default:hover, input[type=submit].default:hover {
562
+ background: var(--default-button-hover-bg);
563
+ }
564
+
565
+ .button[disabled].default,
566
+ input[type=submit][disabled].default,
567
+ input[type=button][disabled].default {
568
+ opacity: 0.4;
569
+ }
570
+
571
+
572
+ /* MODULES */
573
+
574
+ .module {
575
+ border: none;
576
+ margin-bottom: 30px;
577
+ background: var(--body-bg);
578
+ }
579
+
580
+ .module p, .module ul, .module h3, .module h4, .module dl, .module pre {
581
+ padding-left: 10px;
582
+ padding-right: 10px;
583
+ }
584
+
585
+ .module blockquote {
586
+ margin-left: 12px;
587
+ }
588
+
589
+ .module ul, .module ol {
590
+ margin-left: 1.5em;
591
+ }
592
+
593
+ .module h3 {
594
+ margin-top: .6em;
595
+ }
596
+
597
+ .module h2, .module caption, .inline-group h2 {
598
+ margin: 0;
599
+ padding: 8px;
600
+ font-weight: 400;
601
+ font-size: 0.8125rem;
602
+ text-align: left;
603
+ background: var(--header-bg);
604
+ color: var(--header-link-color);
605
+ }
606
+
607
+ .module caption,
608
+ .inline-group h2 {
609
+ font-size: 0.75rem;
610
+ letter-spacing: 0.5px;
611
+ text-transform: uppercase;
612
+ }
613
+
614
+ .module table {
615
+ border-collapse: collapse;
616
+ }
617
+
618
+ /* MESSAGES & ERRORS */
619
+
620
+ ul.messagelist {
621
+ padding: 0;
622
+ margin: 0;
623
+ }
624
+
625
+ ul.messagelist li {
626
+ display: block;
627
+ font-weight: 400;
628
+ font-size: 0.8125rem;
629
+ padding: 10px 10px 10px 65px;
630
+ margin: 0 0 10px 0;
631
+ background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat;
632
+ background-size: 16px auto;
633
+ color: var(--body-fg);
634
+ word-break: break-word;
635
+ }
636
+
637
+ ul.messagelist li.warning {
638
+ background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat;
639
+ background-size: 14px auto;
640
+ }
641
+
642
+ ul.messagelist li.error {
643
+ background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat;
644
+ background-size: 16px auto;
645
+ }
646
+
647
+ .errornote {
648
+ font-size: 0.875rem;
649
+ font-weight: 700;
650
+ display: block;
651
+ padding: 10px 12px;
652
+ margin: 0 0 10px 0;
653
+ color: var(--error-fg);
654
+ border: 1px solid var(--error-fg);
655
+ border-radius: 4px;
656
+ background-color: var(--body-bg);
657
+ background-position: 5px 12px;
658
+ overflow-wrap: break-word;
659
+ }
660
+
661
+ ul.errorlist {
662
+ margin: 0 0 4px;
663
+ padding: 0;
664
+ color: var(--error-fg);
665
+ background: var(--body-bg);
666
+ }
667
+
668
+ ul.errorlist li {
669
+ font-size: 0.8125rem;
670
+ display: block;
671
+ margin-bottom: 4px;
672
+ overflow-wrap: break-word;
673
+ }
674
+
675
+ ul.errorlist li:first-child {
676
+ margin-top: 0;
677
+ }
678
+
679
+ ul.errorlist li a {
680
+ color: inherit;
681
+ text-decoration: underline;
682
+ }
683
+
684
+ td ul.errorlist {
685
+ margin: 0;
686
+ padding: 0;
687
+ }
688
+
689
+ td ul.errorlist li {
690
+ margin: 0;
691
+ }
692
+
693
+ .form-row.errors {
694
+ margin: 0;
695
+ border: none;
696
+ border-bottom: 1px solid var(--hairline-color);
697
+ background: none;
698
+ }
699
+
700
+ .form-row.errors ul.errorlist li {
701
+ padding-left: 0;
702
+ }
703
+
704
+ .errors input, .errors select, .errors textarea,
705
+ td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea {
706
+ border: 1px solid var(--error-fg);
707
+ }
708
+
709
+ .description {
710
+ font-size: 0.75rem;
711
+ padding: 5px 0 0 12px;
712
+ }
713
+
714
+ /* BREADCRUMBS */
715
+
716
+ div.breadcrumbs {
717
+ background: var(--breadcrumbs-bg);
718
+ padding: 10px 40px;
719
+ border: none;
720
+ color: var(--breadcrumbs-fg);
721
+ text-align: left;
722
+ }
723
+
724
+ div.breadcrumbs a {
725
+ color: var(--breadcrumbs-link-fg);
726
+ }
727
+
728
+ div.breadcrumbs a:focus, div.breadcrumbs a:hover {
729
+ color: var(--breadcrumbs-fg);
730
+ }
731
+
732
+ /* ACTION ICONS */
733
+
734
+ .viewlink, .inlineviewlink {
735
+ padding-left: 16px;
736
+ background: url(../img/icon-viewlink.svg) 0 1px no-repeat;
737
+ }
738
+
739
+ .hidelink {
740
+ padding-left: 16px;
741
+ background: url(../img/icon-hidelink.svg) 0 1px no-repeat;
742
+ }
743
+
744
+ .addlink {
745
+ padding-left: 16px;
746
+ background: url(../img/icon-addlink.svg) 0 1px no-repeat;
747
+ }
748
+
749
+ .changelink, .inlinechangelink {
750
+ padding-left: 16px;
751
+ background: url(../img/icon-changelink.svg) 0 1px no-repeat;
752
+ }
753
+
754
+ .deletelink {
755
+ padding-left: 16px;
756
+ background: url(../img/icon-deletelink.svg) 0 1px no-repeat;
757
+ }
758
+
759
+ a.deletelink:link, a.deletelink:visited {
760
+ color: #CC3434; /* XXX Probably unused? */
761
+ }
762
+
763
+ a.deletelink:focus, a.deletelink:hover {
764
+ color: #993333; /* XXX Probably unused? */
765
+ text-decoration: none;
766
+ }
767
+
768
+ /* OBJECT TOOLS */
769
+
770
+ .object-tools {
771
+ font-size: 0.625rem;
772
+ font-weight: bold;
773
+ padding-left: 0;
774
+ float: right;
775
+ position: relative;
776
+ margin-top: -48px;
777
+ }
778
+
779
+ .object-tools li {
780
+ display: block;
781
+ float: left;
782
+ margin-left: 5px;
783
+ height: 1rem;
784
+ }
785
+
786
+ .object-tools a {
787
+ border-radius: 15px;
788
+ }
789
+
790
+ .object-tools a:link, .object-tools a:visited {
791
+ display: block;
792
+ float: left;
793
+ padding: 3px 12px;
794
+ background: var(--object-tools-bg);
795
+ color: var(--object-tools-fg);
796
+ font-weight: 400;
797
+ font-size: 0.6875rem;
798
+ text-transform: uppercase;
799
+ letter-spacing: 0.5px;
800
+ }
801
+
802
+ .object-tools a:focus, .object-tools a:hover {
803
+ background-color: var(--object-tools-hover-bg);
804
+ }
805
+
806
+ .object-tools a:focus{
807
+ text-decoration: none;
808
+ }
809
+
810
+ .object-tools a.viewsitelink, .object-tools a.addlink {
811
+ background-repeat: no-repeat;
812
+ background-position: right 7px center;
813
+ padding-right: 26px;
814
+ }
815
+
816
+ .object-tools a.viewsitelink {
817
+ background-image: url(../img/tooltag-arrowright.svg);
818
+ }
819
+
820
+ .object-tools a.addlink {
821
+ background-image: url(../img/tooltag-add.svg);
822
+ }
823
+
824
+ /* OBJECT HISTORY */
825
+
826
+ #change-history table {
827
+ width: 100%;
828
+ }
829
+
830
+ #change-history table tbody th {
831
+ width: 16em;
832
+ }
833
+
834
+ #change-history .paginator {
835
+ color: var(--body-quiet-color);
836
+ border-bottom: 1px solid var(--hairline-color);
837
+ background: var(--body-bg);
838
+ overflow: hidden;
839
+ }
840
+
841
+ /* PAGE STRUCTURE */
842
+
843
+ #container {
844
+ position: relative;
845
+ width: 100%;
846
+ min-width: 980px;
847
+ padding: 0;
848
+ display: flex;
849
+ flex-direction: column;
850
+ height: 100%;
851
+ }
852
+
853
+ #container > .main {
854
+ display: flex;
855
+ flex: 1 0 auto;
856
+ }
857
+
858
+ .main > .content {
859
+ flex: 1 0;
860
+ max-width: 100%;
861
+ }
862
+
863
+ .skip-to-content-link {
864
+ position: absolute;
865
+ top: -999px;
866
+ margin: 5px;
867
+ padding: 5px;
868
+ background: var(--body-bg);
869
+ z-index: 1;
870
+ }
871
+
872
+ .skip-to-content-link:focus {
873
+ left: 0px;
874
+ top: 0px;
875
+ }
876
+
877
+ #content {
878
+ padding: 20px 40px;
879
+ }
880
+
881
+ .dashboard #content {
882
+ width: 600px;
883
+ }
884
+
885
+ #content-main {
886
+ float: left;
887
+ width: 100%;
888
+ }
889
+
890
+ #content-related {
891
+ float: right;
892
+ width: 260px;
893
+ position: relative;
894
+ margin-right: -300px;
895
+ }
896
+
897
+ @media (forced-colors: active) {
898
+ #content-related {
899
+ border: 1px solid;
900
+ }
901
+ }
902
+
903
+ /* COLUMN TYPES */
904
+
905
+ .colMS {
906
+ margin-right: 300px;
907
+ }
908
+
909
+ .colSM {
910
+ margin-left: 300px;
911
+ }
912
+
913
+ .colSM #content-related {
914
+ float: left;
915
+ margin-right: 0;
916
+ margin-left: -300px;
917
+ }
918
+
919
+ .colSM #content-main {
920
+ float: right;
921
+ }
922
+
923
+ .popup .colM {
924
+ width: auto;
925
+ }
926
+
927
+ /* HEADER */
928
+
929
+ #header {
930
+ width: auto;
931
+ height: auto;
932
+ display: flex;
933
+ justify-content: space-between;
934
+ align-items: center;
935
+ padding: 10px 40px;
936
+ background: var(--header-bg);
937
+ color: var(--header-color);
938
+ }
939
+
940
+ #header a:link, #header a:visited, #logout-form button {
941
+ color: var(--header-link-color);
942
+ }
943
+
944
+ #header a:focus , #header a:hover {
945
+ text-decoration: underline;
946
+ }
947
+
948
+ @media (forced-colors: active) {
949
+ #header {
950
+ border-bottom: 1px solid;
951
+ }
952
+ }
953
+
954
+ #branding {
955
+ display: flex;
956
+ }
957
+
958
+ #site-name {
959
+ padding: 0;
960
+ margin: 0;
961
+ margin-inline-end: 20px;
962
+ font-weight: 300;
963
+ font-size: 1.5rem;
964
+ color: var(--header-branding-color);
965
+ }
966
+
967
+ #site-name a:link, #site-name a:visited {
968
+ color: var(--accent);
969
+ }
970
+
971
+ #branding h2 {
972
+ padding: 0 10px;
973
+ font-size: 0.875rem;
974
+ margin: -8px 0 8px 0;
975
+ font-weight: normal;
976
+ color: var(--header-color);
977
+ }
978
+
979
+ #branding a:hover {
980
+ text-decoration: none;
981
+ }
982
+
983
+ #logout-form {
984
+ display: inline;
985
+ }
986
+
987
+ #logout-form button {
988
+ background: none;
989
+ border: 0;
990
+ cursor: pointer;
991
+ font-family: var(--font-family-primary);
992
+ }
993
+
994
+ #user-tools {
995
+ float: right;
996
+ margin: 0 0 0 20px;
997
+ text-align: right;
998
+ }
999
+
1000
+ #user-tools, #logout-form button{
1001
+ padding: 0;
1002
+ font-weight: 300;
1003
+ font-size: 0.6875rem;
1004
+ letter-spacing: 0.5px;
1005
+ text-transform: uppercase;
1006
+ }
1007
+
1008
+ #user-tools a, #logout-form button {
1009
+ border-bottom: 1px solid rgba(255, 255, 255, 0.25);
1010
+ }
1011
+
1012
+ #user-tools a:focus, #user-tools a:hover,
1013
+ #logout-form button:active, #logout-form button:hover {
1014
+ text-decoration: none;
1015
+ border-bottom: 0;
1016
+ }
1017
+
1018
+ #logout-form button:active, #logout-form button:hover {
1019
+ margin-bottom: 1px;
1020
+ }
1021
+
1022
+ /* SIDEBAR */
1023
+
1024
+ #content-related {
1025
+ background: var(--darkened-bg);
1026
+ }
1027
+
1028
+ #content-related .module {
1029
+ background: none;
1030
+ }
1031
+
1032
+ #content-related h3 {
1033
+ color: var(--body-quiet-color);
1034
+ padding: 0 16px;
1035
+ margin: 0 0 16px;
1036
+ }
1037
+
1038
+ #content-related h4 {
1039
+ font-size: 0.8125rem;
1040
+ }
1041
+
1042
+ #content-related p {
1043
+ padding-left: 16px;
1044
+ padding-right: 16px;
1045
+ }
1046
+
1047
+ #content-related .actionlist {
1048
+ padding: 0;
1049
+ margin: 16px;
1050
+ }
1051
+
1052
+ #content-related .actionlist li {
1053
+ line-height: 1.2;
1054
+ margin-bottom: 10px;
1055
+ padding-left: 18px;
1056
+ }
1057
+
1058
+ #content-related .module h2 {
1059
+ background: none;
1060
+ padding: 16px;
1061
+ margin-bottom: 16px;
1062
+ border-bottom: 1px solid var(--hairline-color);
1063
+ font-size: 1.125rem;
1064
+ color: var(--body-fg);
1065
+ }
1066
+
1067
+ .delete-confirmation form input[type="submit"] {
1068
+ background: var(--delete-button-bg);
1069
+ border-radius: 4px;
1070
+ padding: 10px 15px;
1071
+ color: var(--button-fg);
1072
+ }
1073
+
1074
+ .delete-confirmation form input[type="submit"]:active,
1075
+ .delete-confirmation form input[type="submit"]:focus,
1076
+ .delete-confirmation form input[type="submit"]:hover {
1077
+ background: var(--delete-button-hover-bg);
1078
+ }
1079
+
1080
+ .delete-confirmation form .cancel-link {
1081
+ display: inline-block;
1082
+ vertical-align: middle;
1083
+ height: 0.9375rem;
1084
+ line-height: 0.9375rem;
1085
+ border-radius: 4px;
1086
+ padding: 10px 15px;
1087
+ color: var(--button-fg);
1088
+ background: var(--close-button-bg);
1089
+ margin: 0 0 0 10px;
1090
+ }
1091
+
1092
+ .delete-confirmation form .cancel-link:active,
1093
+ .delete-confirmation form .cancel-link:focus,
1094
+ .delete-confirmation form .cancel-link:hover {
1095
+ background: var(--close-button-hover-bg);
1096
+ }
1097
+
1098
+ /* POPUP */
1099
+ .popup #content {
1100
+ padding: 20px;
1101
+ }
1102
+
1103
+ .popup #container {
1104
+ min-width: 0;
1105
+ }
1106
+
1107
+ .popup #header {
1108
+ padding: 10px 20px;
1109
+ }
1110
+
1111
+ /* PAGINATOR */
1112
+
1113
+ .paginator {
1114
+ display: flex;
1115
+ align-items: center;
1116
+ gap: 4px;
1117
+ font-size: 0.8125rem;
1118
+ padding-top: 10px;
1119
+ padding-bottom: 10px;
1120
+ line-height: 22px;
1121
+ margin: 0;
1122
+ border-top: 1px solid var(--hairline-color);
1123
+ width: 100%;
1124
+ }
1125
+
1126
+ .paginator a:link, .paginator a:visited {
1127
+ padding: 2px 6px;
1128
+ background: var(--button-bg);
1129
+ text-decoration: none;
1130
+ color: var(--button-fg);
1131
+ }
1132
+
1133
+ .paginator a.showall {
1134
+ border: none;
1135
+ background: none;
1136
+ color: var(--link-fg);
1137
+ }
1138
+
1139
+ .paginator a.showall:focus, .paginator a.showall:hover {
1140
+ background: none;
1141
+ color: var(--link-hover-color);
1142
+ }
1143
+
1144
+ .paginator .end {
1145
+ margin-right: 6px;
1146
+ }
1147
+
1148
+ .paginator .this-page {
1149
+ padding: 2px 6px;
1150
+ font-weight: bold;
1151
+ font-size: 0.8125rem;
1152
+ vertical-align: top;
1153
+ }
1154
+
1155
+ .paginator a:focus, .paginator a:hover {
1156
+ color: white;
1157
+ background: var(--link-hover-color);
1158
+ }
1159
+
1160
+ .paginator input {
1161
+ margin-left: auto;
1162
+ }
1163
+
1164
+ .base-svgs {
1165
+ display: none;
1166
+ }
1167
+
1168
+ .visually-hidden {
1169
+ position: absolute;
1170
+ width: 1px;
1171
+ height: 1px;
1172
+ padding: 0;
1173
+ overflow: hidden;
1174
+ clip: rect(0,0,0,0);
1175
+ white-space: nowrap;
1176
+ border: 0;
1177
+ color: var(--body-fg);
1178
+ background-color: var(--body-bg);
1179
+ }
static/admin/css/changelists.css ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CHANGELISTS */
2
+
3
+ #changelist {
4
+ display: flex;
5
+ align-items: flex-start;
6
+ justify-content: space-between;
7
+ }
8
+
9
+ #changelist .changelist-form-container {
10
+ flex: 1 1 auto;
11
+ min-width: 0;
12
+ }
13
+
14
+ #changelist table {
15
+ width: 100%;
16
+ }
17
+
18
+ .change-list .hiddenfields { display:none; }
19
+
20
+ .change-list .filtered table {
21
+ border-right: none;
22
+ }
23
+
24
+ .change-list .filtered {
25
+ min-height: 400px;
26
+ }
27
+
28
+ .change-list .filtered .results, .change-list .filtered .paginator,
29
+ .filtered #toolbar, .filtered div.xfull {
30
+ width: auto;
31
+ }
32
+
33
+ .change-list .filtered table tbody th {
34
+ padding-right: 1em;
35
+ }
36
+
37
+ #changelist-form .results {
38
+ overflow-x: auto;
39
+ width: 100%;
40
+ }
41
+
42
+ #changelist .toplinks {
43
+ border-bottom: 1px solid var(--hairline-color);
44
+ }
45
+
46
+ #changelist .paginator {
47
+ color: var(--body-quiet-color);
48
+ border-bottom: 1px solid var(--hairline-color);
49
+ background: var(--body-bg);
50
+ overflow: hidden;
51
+ }
52
+
53
+ /* CHANGELIST TABLES */
54
+
55
+ #changelist table thead th {
56
+ padding: 0;
57
+ white-space: nowrap;
58
+ vertical-align: middle;
59
+ }
60
+
61
+ #changelist table thead th.action-checkbox-column {
62
+ width: 1.5em;
63
+ text-align: center;
64
+ }
65
+
66
+ #changelist table tbody td.action-checkbox {
67
+ text-align: center;
68
+ }
69
+
70
+ #changelist table tfoot {
71
+ color: var(--body-quiet-color);
72
+ }
73
+
74
+ /* TOOLBAR */
75
+
76
+ #toolbar {
77
+ padding: 8px 10px;
78
+ margin-bottom: 15px;
79
+ border-top: 1px solid var(--hairline-color);
80
+ border-bottom: 1px solid var(--hairline-color);
81
+ background: var(--darkened-bg);
82
+ color: var(--body-quiet-color);
83
+ }
84
+
85
+ #toolbar form input {
86
+ border-radius: 4px;
87
+ font-size: 0.875rem;
88
+ padding: 5px;
89
+ color: var(--body-fg);
90
+ }
91
+
92
+ #toolbar #searchbar {
93
+ height: 1.1875rem;
94
+ border: 1px solid var(--border-color);
95
+ padding: 2px 5px;
96
+ margin: 0;
97
+ vertical-align: top;
98
+ font-size: 0.8125rem;
99
+ max-width: 100%;
100
+ }
101
+
102
+ #toolbar #searchbar:focus {
103
+ border-color: var(--body-quiet-color);
104
+ }
105
+
106
+ #toolbar form input[type="submit"] {
107
+ border: 1px solid var(--border-color);
108
+ font-size: 0.8125rem;
109
+ padding: 4px 8px;
110
+ margin: 0;
111
+ vertical-align: middle;
112
+ background: var(--body-bg);
113
+ box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
114
+ cursor: pointer;
115
+ color: var(--body-fg);
116
+ }
117
+
118
+ #toolbar form input[type="submit"]:focus,
119
+ #toolbar form input[type="submit"]:hover {
120
+ border-color: var(--body-quiet-color);
121
+ }
122
+
123
+ #changelist-search img {
124
+ vertical-align: middle;
125
+ margin-right: 4px;
126
+ }
127
+
128
+ #changelist-search .help {
129
+ word-break: break-word;
130
+ }
131
+
132
+ /* FILTER COLUMN */
133
+
134
+ #changelist-filter {
135
+ flex: 0 0 240px;
136
+ order: 1;
137
+ background: var(--darkened-bg);
138
+ border-left: none;
139
+ margin: 0 0 0 30px;
140
+ }
141
+
142
+ @media (forced-colors: active) {
143
+ #changelist-filter {
144
+ border: 1px solid;
145
+ }
146
+ }
147
+
148
+ #changelist-filter h2 {
149
+ font-size: 0.875rem;
150
+ text-transform: uppercase;
151
+ letter-spacing: 0.5px;
152
+ padding: 5px 15px;
153
+ margin-bottom: 12px;
154
+ border-bottom: none;
155
+ }
156
+
157
+ #changelist-filter h3,
158
+ #changelist-filter details summary {
159
+ font-weight: 400;
160
+ padding: 0 15px;
161
+ margin-bottom: 10px;
162
+ }
163
+
164
+ #changelist-filter details summary > * {
165
+ display: inline;
166
+ }
167
+
168
+ #changelist-filter details > summary {
169
+ list-style-type: none;
170
+ }
171
+
172
+ #changelist-filter details > summary::-webkit-details-marker {
173
+ display: none;
174
+ }
175
+
176
+ #changelist-filter details > summary::before {
177
+ content: '→';
178
+ font-weight: bold;
179
+ color: var(--link-hover-color);
180
+ }
181
+
182
+ #changelist-filter details[open] > summary::before {
183
+ content: '↓';
184
+ }
185
+
186
+ #changelist-filter ul {
187
+ margin: 5px 0;
188
+ padding: 0 15px 15px;
189
+ border-bottom: 1px solid var(--hairline-color);
190
+ }
191
+
192
+ #changelist-filter ul:last-child {
193
+ border-bottom: none;
194
+ }
195
+
196
+ #changelist-filter li {
197
+ list-style-type: none;
198
+ margin-left: 0;
199
+ padding-left: 0;
200
+ }
201
+
202
+ #changelist-filter a {
203
+ display: block;
204
+ color: var(--body-quiet-color);
205
+ word-break: break-word;
206
+ }
207
+
208
+ #changelist-filter li.selected {
209
+ border-left: 5px solid var(--hairline-color);
210
+ padding-left: 10px;
211
+ margin-left: -15px;
212
+ }
213
+
214
+ #changelist-filter li.selected a {
215
+ color: var(--link-selected-fg);
216
+ }
217
+
218
+ #changelist-filter a:focus, #changelist-filter a:hover,
219
+ #changelist-filter li.selected a:focus,
220
+ #changelist-filter li.selected a:hover {
221
+ color: var(--link-hover-color);
222
+ }
223
+
224
+ #changelist-filter #changelist-filter-extra-actions {
225
+ font-size: 0.8125rem;
226
+ margin-bottom: 10px;
227
+ border-bottom: 1px solid var(--hairline-color);
228
+ }
229
+
230
+ /* DATE DRILLDOWN */
231
+
232
+ .change-list .toplinks {
233
+ display: flex;
234
+ padding-bottom: 5px;
235
+ flex-wrap: wrap;
236
+ gap: 3px 17px;
237
+ font-weight: bold;
238
+ }
239
+
240
+ .change-list .toplinks a {
241
+ font-size: 0.8125rem;
242
+ }
243
+
244
+ .change-list .toplinks .date-back {
245
+ color: var(--body-quiet-color);
246
+ }
247
+
248
+ .change-list .toplinks .date-back:focus,
249
+ .change-list .toplinks .date-back:hover {
250
+ color: var(--link-hover-color);
251
+ }
252
+
253
+ /* ACTIONS */
254
+
255
+ .filtered .actions {
256
+ border-right: none;
257
+ }
258
+
259
+ #changelist table input {
260
+ margin: 0;
261
+ vertical-align: baseline;
262
+ }
263
+
264
+ /* Once the :has() pseudo-class is supported by all browsers, the tr.selected
265
+ selector and the JS adding the class can be removed. */
266
+ #changelist tbody tr.selected {
267
+ background-color: var(--selected-row);
268
+ }
269
+
270
+ #changelist tbody tr:has(.action-select:checked) {
271
+ background-color: var(--selected-row);
272
+ }
273
+
274
+ @media (forced-colors: active) {
275
+ #changelist tbody tr.selected {
276
+ background-color: SelectedItem;
277
+ }
278
+ #changelist tbody tr:has(.action-select:checked) {
279
+ background-color: SelectedItem;
280
+ }
281
+ }
282
+
283
+ #changelist .actions {
284
+ padding: 10px;
285
+ background: var(--body-bg);
286
+ border-top: none;
287
+ border-bottom: none;
288
+ line-height: 1.5rem;
289
+ color: var(--body-quiet-color);
290
+ width: 100%;
291
+ }
292
+
293
+ #changelist .actions span.all,
294
+ #changelist .actions span.action-counter,
295
+ #changelist .actions span.clear,
296
+ #changelist .actions span.question {
297
+ font-size: 0.8125rem;
298
+ margin: 0 0.5em;
299
+ }
300
+
301
+ #changelist .actions:last-child {
302
+ border-bottom: none;
303
+ }
304
+
305
+ #changelist .actions select {
306
+ vertical-align: top;
307
+ height: 1.5rem;
308
+ color: var(--body-fg);
309
+ border: 1px solid var(--border-color);
310
+ border-radius: 4px;
311
+ font-size: 0.875rem;
312
+ padding: 0 0 0 4px;
313
+ margin: 0;
314
+ margin-left: 10px;
315
+ }
316
+
317
+ #changelist .actions select:focus {
318
+ border-color: var(--body-quiet-color);
319
+ }
320
+
321
+ #changelist .actions label {
322
+ display: inline-block;
323
+ vertical-align: middle;
324
+ font-size: 0.8125rem;
325
+ }
326
+
327
+ #changelist .actions .button {
328
+ font-size: 0.8125rem;
329
+ border: 1px solid var(--border-color);
330
+ border-radius: 4px;
331
+ background: var(--body-bg);
332
+ box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
333
+ cursor: pointer;
334
+ height: 1.5rem;
335
+ line-height: 1;
336
+ padding: 4px 8px;
337
+ margin: 0;
338
+ color: var(--body-fg);
339
+ }
340
+
341
+ #changelist .actions .button:focus, #changelist .actions .button:hover {
342
+ border-color: var(--body-quiet-color);
343
+ }
static/admin/css/dark_mode.css ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @media (prefers-color-scheme: dark) {
2
+ :root {
3
+ --primary: #264b5d;
4
+ --primary-fg: #f7f7f7;
5
+
6
+ --body-fg: #eeeeee;
7
+ --body-bg: #121212;
8
+ --body-quiet-color: #d0d0d0;
9
+ --body-medium-color: #e0e0e0;
10
+ --body-loud-color: #ffffff;
11
+
12
+ --breadcrumbs-link-fg: #e0e0e0;
13
+ --breadcrumbs-bg: var(--primary);
14
+
15
+ --link-fg: #81d4fa;
16
+ --link-hover-color: #4ac1f7;
17
+ --link-selected-fg: #6f94c6;
18
+
19
+ --hairline-color: #272727;
20
+ --border-color: #353535;
21
+
22
+ --error-fg: #e35f5f;
23
+ --message-success-bg: #006b1b;
24
+ --message-warning-bg: #583305;
25
+ --message-error-bg: #570808;
26
+
27
+ --darkened-bg: #212121;
28
+ --selected-bg: #1b1b1b;
29
+ --selected-row: #00363a;
30
+
31
+ --close-button-bg: #333333;
32
+ --close-button-hover-bg: #666666;
33
+
34
+ color-scheme: dark;
35
+ }
36
+ }
37
+
38
+
39
+ html[data-theme="dark"] {
40
+ --primary: #264b5d;
41
+ --primary-fg: #f7f7f7;
42
+
43
+ --body-fg: #eeeeee;
44
+ --body-bg: #121212;
45
+ --body-quiet-color: #d0d0d0;
46
+ --body-medium-color: #e0e0e0;
47
+ --body-loud-color: #ffffff;
48
+
49
+ --breadcrumbs-link-fg: #e0e0e0;
50
+ --breadcrumbs-bg: var(--primary);
51
+
52
+ --link-fg: #81d4fa;
53
+ --link-hover-color: #4ac1f7;
54
+ --link-selected-fg: #6f94c6;
55
+
56
+ --hairline-color: #272727;
57
+ --border-color: #353535;
58
+
59
+ --error-fg: #e35f5f;
60
+ --message-success-bg: #006b1b;
61
+ --message-warning-bg: #583305;
62
+ --message-error-bg: #570808;
63
+
64
+ --darkened-bg: #212121;
65
+ --selected-bg: #1b1b1b;
66
+ --selected-row: #00363a;
67
+
68
+ --close-button-bg: #333333;
69
+ --close-button-hover-bg: #666666;
70
+
71
+ color-scheme: dark;
72
+ }
73
+
74
+ /* THEME SWITCH */
75
+ .theme-toggle {
76
+ cursor: pointer;
77
+ border: none;
78
+ padding: 0;
79
+ background: transparent;
80
+ vertical-align: middle;
81
+ margin-inline-start: 5px;
82
+ margin-top: -1px;
83
+ }
84
+
85
+ .theme-toggle svg {
86
+ vertical-align: middle;
87
+ height: 1rem;
88
+ width: 1rem;
89
+ display: none;
90
+ }
91
+
92
+ /*
93
+ Fully hide screen reader text so we only show the one matching the current
94
+ theme.
95
+ */
96
+ .theme-toggle .visually-hidden {
97
+ display: none;
98
+ }
99
+
100
+ html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
101
+ display: block;
102
+ }
103
+
104
+ html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
105
+ display: block;
106
+ }
107
+
108
+ html[data-theme="light"] .theme-toggle .theme-label-when-light {
109
+ display: block;
110
+ }
111
+
112
+ /* ICONS */
113
+ .theme-toggle svg.theme-icon-when-auto,
114
+ .theme-toggle svg.theme-icon-when-dark,
115
+ .theme-toggle svg.theme-icon-when-light {
116
+ fill: var(--header-link-color);
117
+ color: var(--header-bg);
118
+ }
119
+
120
+ html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
121
+ display: block;
122
+ }
123
+
124
+ html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
125
+ display: block;
126
+ }
127
+
128
+ html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
129
+ display: block;
130
+ }