diff --git a/.github/workflows/deploy-to-prod.yml b/.github/workflows/deploy-to-prod.yml
new file mode 100644
index 0000000..ad3bd35
--- /dev/null
+++ b/.github/workflows/deploy-to-prod.yml
@@ -0,0 +1,48 @@
+name: Deploy To Production
+
+on:
+ workflow_run:
+ workflows: ["Build and Publish"]
+ types:
+ - completed
+
+jobs:
+ shut-down-current-version:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
+ steps:
+ - name: Retrieving up to date docker-compose.yaml
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.SSH_HOST }}
+ username: ${{ secrets.SSH_USER }}
+ password: ${{ secrets.SSH_PASSWORD }}
+ port: ${{ secrets.SSH_PORT }}
+ script: (cd /home/TheCompound && docker-compose down)
+
+ download-updated-docker-compose:
+ needs: shut-down-current-version
+ runs-on: ubuntu-latest
+ steps:
+ - name: Retrieving up to date docker-compose.yaml
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.SSH_HOST }}
+ username: ${{ secrets.SSH_USER }}
+ password: ${{ secrets.SSH_PASSWORD }}
+ port: ${{ secrets.SSH_PORT }}
+ script: curl -o /home/TheCompound/docker-compose.yml https://raw.githubusercontent.com/SethAngell/TheVirtualCompound/main/.ci/docker-compose.yml
+
+ spin-up-new-version-of-the-compound:
+ needs: [shut-down-current-version, download-updated-docker-compose]
+ if: always()
+ runs-on: ubuntu-latest
+ steps:
+ - name: Retrieving up to date docker-compose.yaml
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.SSH_HOST }}
+ username: ${{ secrets.SSH_USER }}
+ password: ${{ secrets.SSH_PASSWORD }}
+ port: ${{ secrets.SSH_PORT }}
+ script: (cd /home/TheCompound && docker-compose up -d)
diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml
new file mode 100644
index 0000000..0315208
--- /dev/null
+++ b/.github/workflows/notify.yml
@@ -0,0 +1,37 @@
+name: Notify
+
+# Only trigger, when the build workflow succeeded
+on:
+ workflow_run:
+ workflows: ["Deploy To Production"]
+ types:
+ - completed
+
+jobs:
+ see-if-site-is-back-online:
+ name: healthcheck
+ runs-on: ubuntu-latest
+ steps:
+ - name: Give site some time to collectstatic and deploy
+ run: sleep 60s
+ shell: bash
+ - name: Attempt to connect to personal landing page
+ run: curl --connect-timeout 30 --retry 10 --retry-delay 5 https://sethangell.com
+ shell: bash
+
+ send-notification:
+ runs-on: ubuntu-latest
+ needs: see-if-site-is-back-online
+ if: always()
+ steps:
+ - name: Post Pipeline Status
+ uses: twilio-labs/actions-sms@v1
+ with:
+ fromPhoneNumber: ${{ secrets.FROM_NUMBER }}
+ toPhoneNumber: ${{ secrets.TO_NUMBER }}
+ message: "The Virtual Compound CI pipeline is complete. Status of Deployment is ${{ github.event.workflow_run.conclusion }}. Status of post deployment healthcheck is ${{ steps.healthcheck.conclusion }}. See https://sethangell.com"
+ env:
+ TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
+ TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }}
+ TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }}
+
diff --git a/app/TheCompound/settings.py b/app/TheCompound/settings.py
index af9c0c8..bb54251 100644
--- a/app/TheCompound/settings.py
+++ b/app/TheCompound/settings.py
@@ -171,6 +171,13 @@
BASE_DIR / "project_static",
]
+# ===============================
+# = = = = Email Settings = = = =
+if DEBUG is True:
+ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
+else:
+ raise NotImplementedError("Prod Email Is Not Yet Available")
+
# ===============================
# = = = Deployment Settings = = =
if DEBUG is False:
diff --git a/app/TheCompound/urls.py b/app/TheCompound/urls.py
index 6257c09..f7ecf08 100644
--- a/app/TheCompound/urls.py
+++ b/app/TheCompound/urls.py
@@ -23,6 +23,7 @@
path("admin/", admin.site.urls),
path("", include("landing_page.urls")),
path("blog/", include("blog.urls")),
+ path("accounts/", include("accounts.urls")),
]
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/app/accounts/admin.py b/app/accounts/admin.py
index c344989..18bbc2d 100644
--- a/app/accounts/admin.py
+++ b/app/accounts/admin.py
@@ -2,7 +2,7 @@
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserChangeForm, CustomUserCreationForm
-from .models import CustomUser, Domain
+from .models import CustomUser, Domain, Invitation
class CustomUserAdmin(UserAdmin):
@@ -49,5 +49,10 @@ class DomainAdmin(admin.ModelAdmin):
pass
+class InvitationAdmin(admin.ModelAdmin):
+ pass
+
+
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Domain, DomainAdmin)
+admin.site.register(Invitation, InvitationAdmin)
diff --git a/app/accounts/forms.py b/app/accounts/forms.py
index fc37e01..bcc5f57 100644
--- a/app/accounts/forms.py
+++ b/app/accounts/forms.py
@@ -1,6 +1,7 @@
+from django import forms
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
-from .models import CustomUser
+from .models import CustomUser, Invitation
class CustomUserCreationForm(UserCreationForm):
@@ -19,3 +20,17 @@ class Meta:
"email",
"name",
)
+
+
+class NewInvitationForm(forms.ModelForm):
+ class Meta:
+ model = Invitation
+ fields = ("linked_domain", "email")
+
+
+class NewUserFromInviteForm(forms.Form):
+ email = forms.EmailField()
+ name = forms.CharField(max_length=128)
+ invite_code = forms.CharField(max_length=128)
+ password1 = forms.CharField(widget=forms.PasswordInput())
+ password2 = forms.CharField(widget=forms.PasswordInput())
diff --git a/app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py b/app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py
new file mode 100644
index 0000000..856cd1c
--- /dev/null
+++ b/app/accounts/migrations/0004_alter_domain_name_alter_domain_user_invitation.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.0.2 on 2022-07-10 15:51
+
+import uuid
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("accounts", "0003_customuser_name"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domain",
+ name="name",
+ field=models.CharField(max_length=128),
+ ),
+ migrations.AlterField(
+ model_name="domain",
+ name="user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.CreateModel(
+ name="Invitation",
+ fields=[
+ (
+ "email",
+ models.EmailField(
+ max_length=254,
+ primary_key=True,
+ serialize=False,
+ unique=True,
+ verbose_name="email address",
+ ),
+ ),
+ (
+ "invitation_code",
+ models.UUIDField(default=uuid.uuid4, editable=False),
+ ),
+ (
+ "linked_domain",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="accounts.domain",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/app/accounts/models.py b/app/accounts/models.py
index dc3d1c8..ce35368 100644
--- a/app/accounts/models.py
+++ b/app/accounts/models.py
@@ -1,3 +1,5 @@
+import uuid
+
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
@@ -21,8 +23,22 @@ def __str__(self):
class Domain(models.Model):
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True
+ )
name = models.CharField(max_length=128)
def __str__(self):
return self.name
+
+
+class Invitation(models.Model):
+ email = models.EmailField(_("email address"), unique=True, primary_key=True)
+ invitation_code = models.UUIDField(default=uuid.uuid4, editable=False)
+ linked_domain = models.ForeignKey(
+ Domain, on_delete=models.CASCADE, blank=True, null=True
+ )
+
+ def save(self, *args, **kwargs):
+ self.email = self.email.lower()
+ super(Invitation, self).save(*args, **kwargs)
diff --git a/app/accounts/templates/accounts/accept_invite.html b/app/accounts/templates/accounts/accept_invite.html
new file mode 100644
index 0000000..27c8137
--- /dev/null
+++ b/app/accounts/templates/accounts/accept_invite.html
@@ -0,0 +1,101 @@
+{% extends "_base.html" %}
+{% load static %}
+{% load timeline_helper %}
+{% block meta %}
+
+
+
+{% endblock meta %}
+
+{% block links %}
+
+{% endblock %}
+{% block style %}
+ h1, h2, a {
+ font-family: 'Rubik', sans-serif;
+ }
+
+ p {
+ font-family: 'Karla', sans-serif;
+ }
+
+{% endblock %}
+{% block font %}
+
+
+{% endblock %}
+{% block title %}Accept Invite{% endblock title %}
+{% block content %}
+{% if errors %}
+
+{% endif %}
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/app/accounts/templates/accounts/emails/plaintext-invite.txt b/app/accounts/templates/accounts/emails/plaintext-invite.txt
new file mode 100644
index 0000000..6c9cca8
--- /dev/null
+++ b/app/accounts/templates/accounts/emails/plaintext-invite.txt
@@ -0,0 +1,11 @@
+Hey! Welcome To The Site
+
+Create an account here link-to-web.site
+
+Youre unique signup code is {{ invite.invitation_code }}
+
+Make sure to signup with your emails address: {{ invite.email }}
+
+Your site will be available at https://{{ invite.domain.name }}
+
+Bye Now!
\ No newline at end of file
diff --git a/app/accounts/templates/accounts/emails/styled-invite.html b/app/accounts/templates/accounts/emails/styled-invite.html
new file mode 100644
index 0000000..c7ddb1e
--- /dev/null
+++ b/app/accounts/templates/accounts/emails/styled-invite.html
@@ -0,0 +1,11 @@
+Hey! Welcome To The Site
+
+Create an account here link-to-web.site
+
+Youre unique signup code is {{ invite.invitation_code }}
+
+Make sure to signup with your emails address: {{ invite.email }}
+
+Your site will be available at https://{{ invite.linked_domain.name }}
+
+Bye Now!
\ No newline at end of file
diff --git a/app/accounts/templates/accounts/invite_user.html b/app/accounts/templates/accounts/invite_user.html
new file mode 100644
index 0000000..f8b902b
--- /dev/null
+++ b/app/accounts/templates/accounts/invite_user.html
@@ -0,0 +1,64 @@
+{% extends "_base.html" %}
+{% load static %}
+{% load timeline_helper %}
+{% block meta %}
+
+
+
+{% endblock meta %}
+
+{% block links %}
+
+{% endblock %}
+{% block style %}
+ h1, h2, a {
+ font-family: 'Rubik', sans-serif;
+ }
+
+ p {
+ font-family: 'Karla', sans-serif;
+ }
+
+{% endblock %}
+{% block font %}
+
+
+{% endblock %}
+{% block title %}New Invite{% endblock title %}
+{% block content %}
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/app/accounts/urls.py b/app/accounts/urls.py
new file mode 100644
index 0000000..5cc8466
--- /dev/null
+++ b/app/accounts/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("new_invite/", views.create_new_invite, name="new_user_invitation"),
+ path("accept_invite/", views.validate_invited_user, name="accept_invitation"),
+]
diff --git a/app/accounts/views.py b/app/accounts/views.py
index 91ea44a..9ac9f91 100644
--- a/app/accounts/views.py
+++ b/app/accounts/views.py
@@ -1,3 +1,129 @@
-from django.shortcuts import render
+from xml import dom
-# Create your views here.
+from django.contrib.auth import authenticate, login, logout
+from django.core.mail import EmailMultiAlternatives
+from django.http import HttpResponseRedirect
+from django.shortcuts import redirect, render
+from django.template import Context
+from django.template.loader import get_template
+
+from .forms import CustomUserCreationForm, NewInvitationForm, NewUserFromInviteForm
+from .models import CustomUser, Domain, Invitation
+
+
+def create_new_invite(request):
+ # if this is a POST request we need to process the form data
+ if request.method == "POST":
+ # create a form instance and populate it with data from the request:
+ form = NewInvitationForm(request.POST)
+ # check whether it's valid:
+ if form.is_valid():
+ # process the data in form.cleaned_data as required
+ new_invite = form.save()
+ send_invite_email(new_invite)
+ # redirect to a new URL:
+ return HttpResponseRedirect("/")
+ else:
+ print(form.errors)
+
+ # if a GET (or any other method) we'll create a blank form
+ else:
+ form = NewInvitationForm()
+
+ domains = Domain.objects.filter(user=None)
+ context = {"form": form, "domains": domains}
+
+ return render(request, "accounts/invite_user.html", context)
+
+
+def validate_invited_user(request):
+ errors = ""
+ if request.method == "POST":
+ form = NewUserFromInviteForm(request.POST)
+
+ if form.is_valid():
+ user_email = form.cleaned_data["email"].lower()
+ invite_code = form.cleaned_data["invite_code"]
+
+ try:
+ invitation = Invitation.objects.get(email=user_email)
+
+ except Invitation.DoesNotExist:
+ invitation = None
+
+ errors = "Invalid Email: No email address found, make sure you use the same email that your invitation was sent to"
+ context = {"form": form, "errors": errors}
+ return render(request, "accounts/accept_invite.html", context)
+
+ if invitation is not None and (
+ invite_code != str(invitation.invitation_code)
+ ):
+ print(
+ f"IN Code: {invitation.invitation_code} and {len(invitation.invitation_code)}"
+ )
+ print(f"OUT Code: {invite_code} and {len(invite_code)}")
+ errors = "Invalid Invite Code: Make sure you copied that code properly!"
+ elif invitation is not None and invite_code == str(
+ invitation.invitation_code
+ ):
+
+ user_creation_errors, new_user = _create_user(request)
+ if new_user is not None:
+ invitation.linked_domain.user = new_user
+ invitation.linked_domain.save()
+
+ invitation.delete()
+
+ else:
+ if "password2" in user_creation_errors.as_data().keys():
+ errors = (
+ "Password: Oof, that password was way to simple. Try again?"
+ )
+ else:
+ errors = "Invalid User: Failed to create new user. Go bug seth"
+
+ if len(errors) == 0:
+ redirect("home")
+
+ else:
+ errors = form.errors
+
+ form = NewUserFromInviteForm()
+ context = {"form": form, "errors": errors}
+
+ return render(request, "accounts/accept_invite.html", context)
+
+
+def send_invite_email(new_invitation):
+
+ plaintext = get_template("accounts/emails/plaintext-invite.txt")
+ htmly = get_template("accounts/emails/styled-invite.html")
+
+ d = {"invite": new_invitation}
+
+ subject, from_email, to = "Welcome!", "seth@doublel.studio", new_invitation.email
+ text_content = plaintext.render(d)
+ html_content = htmly.render(d)
+ msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+
+
+# ====================================================================================== #
+# Helper Functions
+
+
+def _create_user(request):
+ NewUserForm = CustomUserCreationForm(request.POST)
+
+ if NewUserForm.is_valid():
+ user = NewUserForm.save()
+ raw_password = NewUserForm.cleaned_data.get("password1")
+ user = authenticate(request, email=user.email, password=raw_password)
+
+ if user is not None:
+ login(request, user)
+
+ return (None, user)
+ else:
+ return (NewUserForm.errors, None)
diff --git a/app/blog/migrations/0004_alter_blogpost_parent_blog.py b/app/blog/migrations/0004_alter_blogpost_parent_blog.py
new file mode 100644
index 0000000..42afa53
--- /dev/null
+++ b/app/blog/migrations/0004_alter_blogpost_parent_blog.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.0.2 on 2022-07-10 15:51
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("blog", "0003_blogpost_parent_blog_alter_blog_blog_description"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="blogpost",
+ name="parent_blog",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="blog.blog"
+ ),
+ ),
+ ]
diff --git a/app/jstoolchain/package-lock.json b/app/jstoolchain/package-lock.json
index 831abdd..2f7fbcd 100644
--- a/app/jstoolchain/package-lock.json
+++ b/app/jstoolchain/package-lock.json
@@ -12,6 +12,7 @@
"tailwindcss": "^3.1.4"
},
"devDependencies": {
+ "@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2"
}
},
@@ -47,6 +48,18 @@
"node": ">= 8"
}
},
+ "node_modules/@tailwindcss/forms": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz",
+ "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==",
+ "dev": true,
+ "dependencies": {
+ "mini-svg-data-uri": "^1.2.3"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
+ }
+ },
"node_modules/@tailwindcss/typography": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz",
@@ -636,6 +649,15 @@
"node": ">=8.6"
}
},
+ "node_modules/mini-svg-data-uri": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
+ "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
+ "dev": true,
+ "bin": {
+ "mini-svg-data-uri": "cli.js"
+ }
+ },
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
@@ -1245,6 +1267,15 @@
"fastq": "^1.6.0"
}
},
+ "@tailwindcss/forms": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz",
+ "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==",
+ "dev": true,
+ "requires": {
+ "mini-svg-data-uri": "^1.2.3"
+ }
+ },
"@tailwindcss/typography": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz",
@@ -1639,6 +1670,12 @@
"picomatch": "^2.3.1"
}
},
+ "mini-svg-data-uri": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
+ "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
+ "dev": true
+ },
"minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
diff --git a/app/jstoolchain/package.json b/app/jstoolchain/package.json
index ae68a87..9f8adf1 100644
--- a/app/jstoolchain/package.json
+++ b/app/jstoolchain/package.json
@@ -10,6 +10,7 @@
"tailwindcss": "^3.1.4"
},
"devDependencies": {
+ "@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2"
}
}
diff --git a/app/jstoolchain/tailwind.config.js b/app/jstoolchain/tailwind.config.js
index 26a12a7..2208731 100644
--- a/app/jstoolchain/tailwind.config.js
+++ b/app/jstoolchain/tailwind.config.js
@@ -17,6 +17,7 @@ module.exports = {
},
teal: colors.teal,
slate: colors.slate,
+ pink: colors.pink,
},
extend: {},
diff --git a/app/project_static/css/tailwind-output.css b/app/project_static/css/tailwind-output.css
index 19f4a2f..eb9e137 100644
--- a/app/project_static/css/tailwind-output.css
+++ b/app/project_static/css/tailwind-output.css
@@ -1047,6 +1047,9 @@ video {
--tw-prose-invert-th-borders: #475569;
--tw-prose-invert-td-borders: #334155;
}
+.invisible {
+ visibility: hidden;
+}
.static {
position: static;
}
@@ -1077,6 +1080,15 @@ video {
.mt-5 {
margin-top: 1.25rem;
}
+.mb-5 {
+ margin-bottom: 1.25rem;
+}
+.mt-1 {
+ margin-top: 0.25rem;
+}
+.mt-2 {
+ margin-top: 0.5rem;
+}
.mt-3 {
margin-top: 0.75rem;
}
@@ -1104,6 +1116,14 @@ video {
.h-screen {
height: 100vh;
}
+.h-fit {
+ height: -webkit-fit-content;
+ height: -moz-fit-content;
+ height: fit-content;
+}
+.h-6 {
+ height: 1.5rem;
+}
.h-full {
height: 100%;
}
@@ -1113,14 +1133,17 @@ video {
.w-11\/12 {
width: 91.666667%;
}
-.w-full {
- width: 100%;
-}
.w-fit {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
+.w-6 {
+ width: 1.5rem;
+}
+.w-full {
+ width: 100%;
+}
.w-44 {
width: 11rem;
}
@@ -1142,6 +1165,11 @@ video {
.basis-1\/2 {
flex-basis: 50%;
}
+.appearance-none {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -1154,6 +1182,9 @@ video {
.content-center {
align-content: center;
}
+.items-start {
+ align-items: flex-start;
+}
.items-center {
align-items: center;
}
@@ -1169,6 +1200,11 @@ video {
.gap-y-2 {
row-gap: 0.5rem;
}
+.space-x-1 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.25rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
+}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@@ -1189,23 +1225,34 @@ video {
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
+.rounded-lg {
+ border-radius: 0.5rem;
+}
.rounded-md {
border-radius: 0.375rem;
}
-.rounded-lg {
- border-radius: 0.5rem;
+.rounded-full {
+ border-radius: 9999px;
}
.rounded {
border-radius: 0.25rem;
}
-.rounded-full {
- border-radius: 9999px;
+.border-2 {
+ border-width: 2px;
+}
+.border {
+ border-width: 1px;
}
.border-4 {
border-width: 4px;
}
-.border-2 {
- border-width: 2px;
+.border-seth-blue-600 {
+ --tw-border-opacity: 1;
+ border-color: rgb(5 5 107 / var(--tw-border-opacity));
+}
+.border-slate-300 {
+ --tw-border-opacity: 1;
+ border-color: rgb(203 213 225 / var(--tw-border-opacity));
}
.border-slate-800 {
--tw-border-opacity: 1;
@@ -1219,6 +1266,10 @@ video {
--tw-bg-opacity: 1;
background-color: rgb(248 250 252 / var(--tw-bg-opacity));
}
+.bg-slate-500 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(100 116 139 / var(--tw-bg-opacity));
+}
.bg-slate-800 {
--tw-bg-opacity: 1;
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
@@ -1243,12 +1294,27 @@ video {
-o-object-fit: cover;
object-fit: cover;
}
+.p-10 {
+ padding: 2.5rem;
+}
.p-8 {
padding: 2rem;
}
.p-2 {
padding: 0.5rem;
}
+.px-3 {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+}
+.py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+.px-5 {
+ padding-left: 1.25rem;
+ padding-right: 1.25rem;
+}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1261,14 +1327,6 @@ video {
padding-left: 1rem;
padding-right: 1rem;
}
-.px-5 {
- padding-left: 1.25rem;
- padding-right: 1.25rem;
-}
-.py-2 {
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
-}
.px-8 {
padding-left: 2rem;
padding-right: 2rem;
@@ -1297,6 +1355,10 @@ video {
.text-center {
text-align: center;
}
+.text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
@@ -1317,26 +1379,41 @@ video {
font-size: 1.25rem;
line-height: 1.75rem;
}
+.font-medium {
+ font-weight: 500;
+}
+.font-semibold {
+ font-weight: 600;
+}
.font-bold {
font-weight: 700;
}
.font-black {
font-weight: 900;
}
-.font-semibold {
- font-weight: 600;
-}
.leading-5 {
line-height: 1.25rem;
}
-.text-slate-800 {
+.text-slate-900 {
--tw-text-opacity: 1;
- color: rgb(30 41 59 / var(--tw-text-opacity));
+ color: rgb(15 23 42 / var(--tw-text-opacity));
+}
+.text-slate-700 {
+ --tw-text-opacity: 1;
+ color: rgb(51 65 85 / var(--tw-text-opacity));
+}
+.text-pink-600 {
+ --tw-text-opacity: 1;
+ color: rgb(219 39 119 / var(--tw-text-opacity));
}
.text-slate-50 {
--tw-text-opacity: 1;
color: rgb(248 250 252 / var(--tw-text-opacity));
}
+.text-slate-800 {
+ --tw-text-opacity: 1;
+ color: rgb(30 41 59 / var(--tw-text-opacity));
+}
.text-seth-blue-400 {
--tw-text-opacity: 1;
color: rgb(5 56 107 / var(--tw-text-opacity));
@@ -1349,6 +1426,19 @@ video {
--tw-text-opacity: 1;
color: rgb(226 232 240 / var(--tw-text-opacity));
}
+.placeholder-slate-400::-moz-placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(148 163 184 / var(--tw-placeholder-opacity));
+}
+.placeholder-slate-400::placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(148 163 184 / var(--tw-placeholder-opacity));
+}
+.shadow-sm {
+ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
.shadow-2xl {
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
@@ -1366,6 +1456,14 @@ video {
.ring-offset-4 {
--tw-ring-offset-width: 4px;
}
+.invalid\:border-pink-500:invalid {
+ --tw-border-opacity: 1;
+ border-color: rgb(236 72 153 / var(--tw-border-opacity));
+}
+.invalid\:text-pink-600:invalid {
+ --tw-text-opacity: 1;
+ color: rgb(219 39 119 / var(--tw-text-opacity));
+}
.hover\:bg-slate-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(71 85 105 / var(--tw-bg-opacity));
@@ -1395,6 +1493,56 @@ video {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
+.focus\:outline-none:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+.focus\:ring-1:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+.focus\:ring:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+.focus\:ring-pink-300:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(249 168 212 / var(--tw-ring-opacity));
+}
+.focus\:invalid\:border-pink-500:invalid:focus {
+ --tw-border-opacity: 1;
+ border-color: rgb(236 72 153 / var(--tw-border-opacity));
+}
+.focus\:invalid\:ring-pink-500:invalid:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(236 72 153 / var(--tw-ring-opacity));
+}
+.active\:bg-slate-700:active {
+ --tw-bg-opacity: 1;
+ background-color: rgb(51 65 85 / var(--tw-bg-opacity));
+}
+.disabled\:border-slate-200:disabled {
+ --tw-border-opacity: 1;
+ border-color: rgb(226 232 240 / var(--tw-border-opacity));
+}
+.disabled\:bg-slate-50:disabled {
+ --tw-bg-opacity: 1;
+ background-color: rgb(248 250 252 / var(--tw-bg-opacity));
+}
+.disabled\:text-slate-500:disabled {
+ --tw-text-opacity: 1;
+ color: rgb(100 116 139 / var(--tw-text-opacity));
+}
+.disabled\:shadow-none:disabled {
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+.peer:invalid ~ .peer-invalid\:visible {
+ visibility: visible;
+}
@media (prefers-color-scheme: dark) {
.dark\:prose-invert {
@@ -1480,6 +1628,13 @@ video {
color: rgb(148 163 184 / var(--tw-text-opacity));
}
}
+@media (min-width: 640px) {
+
+ .sm\:text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ }
+}
@media (min-width: 768px) {
.md\:grid-cols-3 {
@@ -1504,6 +1659,10 @@ video {
height: min-content;
}
+ .lg\:w-3\/6 {
+ width: 50%;
+ }
+
.lg\:w-3\/5 {
width: 60%;
}