Implemented search

This commit is contained in:
Kristofers Solo 2023-06-29 16:41:10 +00:00
parent 7e8bf84c0b
commit 313ff02f25
16 changed files with 250 additions and 108 deletions

View File

@ -2,6 +2,5 @@ Django==4.2.2
Pillow==9.5.0 Pillow==9.5.0
fontawesomefree==6.4.0 fontawesomefree==6.4.0
mysqlclient==2.1.1 mysqlclient==2.1.1
django-filter==23.2
django-tailwind==3.6.0 django-tailwind==3.6.0
django-crispy-forms==2.0 django-search-views-0.3.7

View File

@ -38,10 +38,9 @@ INSTALLED_APPS = [
"main", "main",
"account", "account",
"fossdb", "fossdb",
"django_filters", "search_views",
"tailwind", "tailwind",
"tokyonight_night", "tokyonight_night",
"crispy_forms",
"fontawesomefree", "fontawesomefree",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
@ -51,6 +50,13 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
] ]
TAILWIND_APP_NAME = "tokyonight_night"
INTERNAL_IPS = [
"127.0.0.1",
]
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -157,12 +163,6 @@ LOGOUT_REDIRECT_URL = "/"
LOGIN_URL = "/login/" LOGIN_URL = "/login/"
TAILWIND_APP_NAME = "tokyonight_night"
INTERNAL_IPS = [
"127.0.0.1",
]
# HTTPS settings # HTTPS settings
# SESSION_COOKIE_SECURE = True # SESSION_COOKIE_SECURE = True
# CSRF_COOKIE_SECURE = True # CSRF_COOKIE_SECURE = True

View File

@ -24,7 +24,7 @@
id="submit-button" id="submit-button"
class="button submit-button-disabled text-lightsteelblue-100 bg-slategray-200">Login</button> class="button submit-button-disabled text-lightsteelblue-100 bg-slategray-200">Login</button>
<p> <p>
Don't have an account? Create one <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-300 ease-in-out" Don't have an account? Create one <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-300 ease-linear"
href="{% url 'signup' %}">Here</a>! href="{% url 'signup' %}">Here</a>!
</p> </p>
</form> </form>

View File

@ -1,28 +0,0 @@
import django_filters
from django.contrib.auth import get_user_model
from .models import Project
User = get_user_model()
class UserFilter(django_filters.FilterSet):
username = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = User
fields = ("username",)
class ProjectFilter(django_filters.FilterSet):
owner = UserFilter()
name = django_filters.CharFilter(lookup_expr="icontains")
description = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = Project
fields = (
"owner",
"name",
"description",
)

View File

@ -1,20 +1,33 @@
from django import forms from django import forms
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from .models import HostingPlatform, ProgrammingLanguage, Project, ProjectHostingPlatform, ProjectProgrammingLanguage from .models import HostingPlatform, License, OperatingSystemVersion, ProgrammingLanguage, Project, ProjectHostingPlatform, ProjectProgrammingLanguage, Tag
class HostingPlatformForm(forms.ModelForm): class HostingPlatformForm(forms.ModelForm):
class Meta: class Meta:
model = ProjectHostingPlatform model = ProjectHostingPlatform
fields = ( fields = (
"url",
"hosting_platform", "hosting_platform",
"url",
) )
widgets = { widgets = {
"hosting_platform": forms.Select( "hosting_platform": forms.Select(
choices=HostingPlatform.objects.all(), choices=HostingPlatform.objects.all(),
) attrs={
"class": "form-field submit-form",
},
),
"url": forms.URLInput(
attrs={
"placeholder": "url",
"class": "form-field submit-form font-roboto",
},
),
}
labels = {
"hosting_platform": "",
"url": "",
} }
@ -28,14 +41,32 @@ class ProgrammingLanguageForm(forms.ModelForm):
widgets = { widgets = {
"programming_language": forms.Select( "programming_language": forms.Select(
choices=ProgrammingLanguage.objects.all(), choices=ProgrammingLanguage.objects.all(),
attrs={
"class": "form-field submit-form",
},
), ),
"percentage": forms.NumberInput( "percentage": forms.NumberInput(
attrs={ attrs={
"placeholder": "Percentage",
"class": "form-field submit-form",
"min": "0", "min": "0",
"max": "100", "max": "100",
} },
), ),
} }
labels = {
"programming_language": "",
"percentage": "",
}
ProgrammingLanguageInlineFormSet = inlineformset_factory(
Project,
ProjectProgrammingLanguage,
form=ProgrammingLanguageForm,
extra=1,
can_delete=True,
)
class ProjectForm(forms.ModelForm): class ProjectForm(forms.ModelForm):
@ -52,29 +83,39 @@ class ProjectForm(forms.ModelForm):
widgets = { widgets = {
"name": forms.TextInput( "name": forms.TextInput(
attrs={ attrs={
"class": "form-control",
"placeholder": "Project name", "placeholder": "Project name",
} "class": "form-field submit-form font-roboto",
},
), ),
"description": forms.Textarea( "description": forms.Textarea(
attrs={ attrs={
"class": "form-control",
"placeholder": "Description", "placeholder": "Description",
} "class": "form-field submit-form font-roboto",
},
),
"license": forms.CheckboxSelectMultiple(
choices=License.objects.all(),
attrs={
"class": "checkbox-form",
},
),
"operating_system": forms.CheckboxSelectMultiple(
choices=OperatingSystemVersion.objects.all(),
attrs={
"class": "checkbox-form",
},
),
"tag": forms.CheckboxSelectMultiple(
choices=Tag.objects.all(),
attrs={
"class": "checkbox-form",
},
), ),
# "license": forms.CheckboxSelectMultiple(),
# "tag": forms.CheckboxSelectMultiple(),
# "operating_system": forms.CheckboxSelectMultiple(),
} }
labels = {
"name": "",
ProgrammingLanguageInlineFormSet = inlineformset_factory( "description": "",
Project, "license": "",
ProjectProgrammingLanguage, "tag": "",
fields=( "operating_system": "",
"programming_language", }
"percentage",
),
extra=1,
can_delete=True,
)

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-29 14:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0005_operatingsystem_is_linux'),
]
operations = [
migrations.AlterField(
model_name='projectprogramminglanguage',
name='percentage',
field=models.PositiveIntegerField(blank=True, default=0),
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 4.2.2 on 2023-06-29 15:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0006_alter_projectprogramminglanguage_percentage'),
]
operations = [
migrations.AlterField(
model_name='license',
name='full_name',
field=models.CharField(db_index=True, max_length=100, unique=True),
),
migrations.AlterField(
model_name='license',
name='short_name',
field=models.CharField(db_index=True, max_length=50, unique=True),
),
migrations.AlterField(
model_name='operatingsystem',
name='name',
field=models.CharField(db_index=True, max_length=100, unique=True),
),
migrations.AlterField(
model_name='programminglanguage',
name='name',
field=models.CharField(db_index=True, max_length=100, unique=True),
),
migrations.AlterField(
model_name='project',
name='description',
field=models.TextField(blank=True, db_index=True, default=''),
),
migrations.AlterField(
model_name='project',
name='name',
field=models.CharField(db_index=True, max_length=255),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-29 15:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0007_alter_license_full_name_alter_license_short_name_and_more'),
]
operations = [
migrations.AlterField(
model_name='project',
name='description',
field=models.TextField(blank=True, default=''),
),
]

View File

@ -6,8 +6,8 @@ from django.db import models
class License(models.Model): class License(models.Model):
short_name = models.CharField(max_length=50, unique=True) short_name = models.CharField(max_length=50, unique=True, db_index=True)
full_name = models.CharField(max_length=100, unique=True) full_name = models.CharField(max_length=100, unique=True, db_index=True)
url = models.URLField(blank=True, default="") url = models.URLField(blank=True, default="")
text = models.TextField(blank=True, default="") text = models.TextField(blank=True, default="")
@ -16,7 +16,7 @@ class License(models.Model):
class OperatingSystem(models.Model): class OperatingSystem(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True, db_index=True)
description = models.TextField(blank=True, default="") description = models.TextField(blank=True, default="")
is_linux = models.BooleanField(blank=True, default=False) is_linux = models.BooleanField(blank=True, default=False)
@ -55,7 +55,7 @@ class Tag(models.Model):
class ProgrammingLanguage(models.Model): class ProgrammingLanguage(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True, db_index=True)
def __str__(self): def __str__(self):
return self.name return self.name
@ -64,7 +64,7 @@ class ProgrammingLanguage(models.Model):
class Project(models.Model): class Project(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name="ID") id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name="ID")
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255, db_index=True)
description = models.TextField(blank=True, default="") description = models.TextField(blank=True, default="")
license = models.ManyToManyField(License, blank=True) license = models.ManyToManyField(License, blank=True)
tag = models.ManyToManyField(Tag, blank=True) tag = models.ManyToManyField(Tag, blank=True)
@ -117,7 +117,7 @@ class Project(models.Model):
class ProjectProgrammingLanguage(models.Model): class ProjectProgrammingLanguage(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE)
programming_language = models.ForeignKey(ProgrammingLanguage, on_delete=models.CASCADE) programming_language = models.ForeignKey(ProgrammingLanguage, on_delete=models.CASCADE)
percentage = models.PositiveIntegerField(blank=True, null=True) percentage = models.PositiveIntegerField(blank=True, default=0)
def __str__(self): def __str__(self):
return f"{self.project.owner}/{self.project.name} | {self.programming_language} | {self.percentage}%" return f"{self.project.owner}/{self.project.name} | {self.programming_language} | {self.percentage}%"

View File

@ -13,33 +13,33 @@
</h2> </h2>
<!-- os platform icons --> <!-- os platform icons -->
<div class="my-4 flex justify-center items-center gap-x-4"> <div class="my-4 flex justify-center items-center gap-x-4">
<a href=""><i class="fa-brands fa-linux fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out <a href="/search/?q=linux"><i class="fa-brands fa-linux fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-linear
{% if project.runs_on_linux %} {% if project.runs_on_linux %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}"></i></a>
<a href=""><i class="fa-brands fa-windows fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out <a href="/search/?q=windows"><i class="fa-brands fa-windows fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-linear
{% if project.runs_on_windows %} {% if project.runs_on_windows %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}"></i></a>
<a href=""><i class="fa-brands fa-apple fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out <a href="/search/?q=macos"><i class="fa-brands fa-apple fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-linear
{% if project.runs_on_macos %} {% if project.runs_on_macos %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}"></i></a>
<a href=""><i id="ios" <a href="/search/?q=ios"><i id="ios"
class="fa-brands fa-app-store-ios fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out class="fa-brands fa-app-store-ios fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-linear
{% if project.runs_on_ios %} {% if project.runs_on_ios %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
text-slategray-200 text-slategray-200
{% endif %}"></i></a> {% endif %}"></i></a>
<a href=""><i id="android" <a href="/search/?q=android"><i id="android"
class="fa-brands fa-android fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out class="fa-brands fa-android fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-linear
{% if project.runs_on_android %} {% if project.runs_on_android %}
text-lightsteelblue-100 text-lightsteelblue-100
{% else %} {% else %}
@ -49,9 +49,9 @@
<!-- tags --> <!-- tags -->
<div class="my-8 flex flex-wrap justify-center items-start gap-2"> <div class="my-8 flex flex-wrap justify-center items-start gap-2">
{% for tag in project.tag.all|dictsort:"name" %} {% for tag in project.tag.all|dictsort:"name" %}
<a href=""> <a href="/search/?q={{ tag.name }}">
<span title="{{ tag.description }}" <span title="{{ tag.description }}"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-in-out"> class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-linear">
{{ tag }} {{ tag }}
</span> </span>
</a> </a>
@ -60,9 +60,9 @@
<!-- programming languages --> <!-- programming languages -->
<div class="my-8 flex flex-wrap justify-center items-start gap-2"> <div class="my-8 flex flex-wrap justify-center items-start gap-2">
{% for language in project.projectprogramminglanguage_set.all|dictsortreversed:"percentage" %} {% for language in project.projectprogramminglanguage_set.all|dictsortreversed:"percentage" %}
<a href=""> <a href="/search/?q={{ language.name }}">
<span title="{{ language.percentage }}%" <span title="{{ language.percentage }}%"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-in-out"> class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-linear">
{{ language.programming_language }} {{ language.programming_language }}
</span> </span>
</a> </a>
@ -75,7 +75,7 @@
<div class="flex justify-between w-full mt-4 font-abel"> <div class="flex justify-between w-full mt-4 font-abel">
<!-- licenses --> <!-- licenses -->
<div> <div>
<button class="text-xl underline font-abel hover:text-lightsteelblue-200 transform duration-300 ease-in-out" <button class="text-xl underline font-abel hover:text-lightsteelblue-200 transform duration-300 ease-linear"
id="menu-button" id="menu-button"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true">Licenses</button> aria-expanded="true">Licenses</button>
@ -83,7 +83,7 @@
<!-- hosting platform --> <!-- hosting platform -->
<div class=""> <div class="">
<p> <p>
See project source code and read more <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-300 ease-in-out" See project source code and read more <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-300 ease-linear"
href="{{ project.projecthostingplatform.url }}" href="{{ project.projecthostingplatform.url }}"
target="_blank">here</a>! target="_blank">here</a>!
</p> </p>

View File

@ -3,13 +3,20 @@
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %} {% block meta %}{% endblock %}
{% block content %} {% block content %}
{% for project in projects %} <div class="py-8 px-16">
<div> <h1 class="font-abel text-4xl mb-8">Projects</h1>
<a href="{{ project.get_absolute_url }}">{{ project }}</a> <div class="grid grid-cols-2 gap-4">
{% for project in projects %}
<div class="border border-steelblue-100 p-8 flex flex-row gap-4 justify-between">
<a href="{{ project.get_absolute_url }}"
class="hover:text-steelblue-100 transition duration-300 ease-linear">
<h2 class="font-abel text-2xl">{{ project.name }}</h2>
</a>
<p class="max-w-xs text-justify">{{ project.description|slice:500 }}</p>
</div>
{% empty %}
<p>No projects yet.</p>
{% endfor %}
</div> </div>
<div>{{ project.description }}</div> </div>
<hr />
{% empty %}
<p>No projects found</p>
{% endfor %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
<div class="py-8 px-32">
<h1 class="font-abel text-4xl mb-8">Search Results</h1>
<div class="grid grid-cols-1 gap-4">
{% for project in projects %}
<div class="border border-skyblue-300 p-8 flex flex-row gap-4 justify-between">
<a href="{{ project.get_absolute_url }}"
class="hover:text-skyblue-300 transition duration-300 ease-linear">
<h2 class="font-abel text-2xl">{{ project.name }}</h2>
</a>
<p class="max-w-xs text-justify">{{ project.description|slice:500 }}</p>
</div>
{% empty %}
<p>No projects yet.</p>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -3,6 +3,7 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("search/", views.SearchResultsListView.as_view(), name="search"),
path("explore/", views.ProjectListView.as_view(), name="explore"), path("explore/", views.ProjectListView.as_view(), name="explore"),
path("contribute/", views.ProjectCreateView.as_view(), name="contribute"), path("contribute/", views.ProjectCreateView.as_view(), name="contribute"),
path("<str:owner>/<str:project_name>/", views.ProjectDetailView.as_view(), name="project-detail"), path("<str:owner>/<str:project_name>/", views.ProjectDetailView.as_view(), name="project-detail"),

View File

@ -1,21 +1,39 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import Q
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView from django.urls import reverse_lazy
from django_filters.views import FilterView from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from .filters import ProjectFilter
from .forms import HostingPlatformForm, ProgrammingLanguageInlineFormSet, ProjectForm from .forms import HostingPlatformForm, ProgrammingLanguageInlineFormSet, ProjectForm
from .models import Project from .models import Project
class ProjectListView(FilterView): class SearchResultsListView(ListView):
model = Project
template_name = "search.html"
context_object_name = "projects"
def get_queryset(self):
query = self.request.GET.get("q")
return Project.objects.filter(
Q(owner__username__icontains=query)
| Q(name__icontains=query)
# | Q(description__icontains=query)
| Q(license__short_name__icontains=query)
| Q(license__full_name__icontains=query)
| Q(tag__name__icontains=query)
| Q(operating_system__operating_system__name__icontains=query)
| Q(operating_system__codename__icontains=query)
| Q(programming_language__name__icontains=query)
).distinct()
class ProjectListView(ListView):
model = Project model = Project
template_name = "explore.html" template_name = "explore.html"
filterset_class = ProjectFilter
context_object_name = "projects" context_object_name = "projects"
paginate_by = 100 # optional 10 projects a page paginate_by = 50 # amount of items on screen
class ProjectCreateView(LoginRequiredMixin, CreateView): class ProjectCreateView(LoginRequiredMixin, CreateView):
@ -108,7 +126,7 @@ class ProjectDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
slug_url_kwarg = "project_name" slug_url_kwarg = "project_name"
login_url = "/login/" login_url = "/login/"
redirect_field_name = "redirect_to" redirect_field_name = "redirect_to"
success_url = "/" success_url = reverse_lazy("homepage")
def test_func(self): def test_func(self):
return self.get_object().owner == self.request.user return self.get_object().owner == self.request.user

View File

@ -38,7 +38,7 @@ body {
} }
.button { .button {
@apply font-abel px-12 py-2 border rounded-md transform ease-in-out duration-500; @apply font-abel px-12 py-2 border rounded-md transform ease-linear duration-500;
} }
.submit-button-disabled { .submit-button-disabled {

View File

@ -32,27 +32,30 @@
width="40" width="40"
src="{% static 'img/icons/logo.svg' %}" src="{% static 'img/icons/logo.svg' %}"
alt="logo" /> alt="logo" />
<a class="hover:text-skyblue-300 transform duration-300 ease-in-out" <a class="hover:text-skyblue-300 transform duration-300 ease-linear"
href="{% url 'homepage' %}">foss<span class="text-skyblue-300">db</span></a> href="{% url 'homepage' %}">foss<span class="text-skyblue-300">db</span></a>
</div> </div>
<!-- search --> <!-- search -->
<div class="relative items-center flex"> <form action="{% url 'search' %}"
method="get"
class="relative items-center flex">
<input type="text" <input type="text"
name="q"
placeholder="Search projects..." placeholder="Search projects..."
class="py-2 mx-4 placeholder-slategray-100 bg-gray-300 border-0 border-b-2 border-b-slategray-100 hover:border-b-lightsteelblue-100 transform duration-200 ease-in-out focus:outline-none focus:border-b-skyblue-300 focus:border-b-2 max-w-[10rem]" /> class="py-2 mx-4 placeholder-slategray-100 bg-gray-300 border-0 border-b-2 border-b-slategray-100 hover:border-b-lightsteelblue-100 transform duration-200 ease-linear focus:outline-none focus:border-b-skyblue-300 focus:border-b-2 max-w-[10rem]" />
</div> </form>
<!-- navbar --> <!-- navbar -->
<nav class="uppercase flex gap-x-6 items-center"> <nav class="uppercase flex gap-x-6 items-center">
<a href="{% url 'explore' %}" <a href="{% url 'explore' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">explore</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">explore</a>
<a href="{% url 'contribute' %}" <a href="{% url 'contribute' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">contribute</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">contribute</a>
<a href="{% url 'news' %}" <a href="{% url 'news' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">news</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">news</a>
<a href="{% url 'dashboard' %}" <a href="{% url 'dashboard' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">dashboard</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">dashboard</a>
<a href="{% url 'help' %}" <a href="{% url 'help' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">help</a> class="hover:text-skyblue-300 transform duration-200 ease-linear">help</a>
</nav> </nav>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -68,7 +71,7 @@
</div> </div>
{% else %} {% else %}
<a href="{% url 'login' %}" <a href="{% url 'login' %}"
class="hover:text-skyblue-300 transition duration-300 ease-in-out"><i class="fa-solid fa-user fa-lg ml-4 "></i></a> class="hover:text-skyblue-300 transition duration-300 ease-linear"><i class="fa-solid fa-user fa-lg ml-4 "></i></a>
{% endif %} {% endif %}
</div> </div>
</header> </header>