Skip to content

Commit

Permalink
First Time User Experience (#502)
Browse files Browse the repository at this point in the history
* Add First Time User Experience view
* Add First Time User Experience component sub-views
* Add initial workings of Thunderbird Pro Services components
* Switch Dayjs injection to use a typed symbol
* Add typescript to eslint
* Begin tailwind's demise!
* 🌐 Update German language
* Lots of re-organization

---------

Co-authored-by: Andreas Müller <[email protected]>
  • Loading branch information
MelissaAutumn and devmount authored Jul 2, 2024
1 parent 72ba61e commit 6f58211
Show file tree
Hide file tree
Showing 169 changed files with 3,833 additions and 235 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ charset = utf-8
indent_style = space
indent_size = 4

[*.{vue,html,css,js,json,yml}]
[*.{vue,html,css,js,json,yml,ts}]
charset = utf-8
indent_style = space
indent_size = 2
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ nh3==0.2.17
python-dotenv==1.0.1
python-multipart==0.0.9
PyJWT==2.6.0
pydantic==2.7.4
pydantic[email]==2.7.4
sentry-sdk==2.7.1
starlette-context==0.3.6
sqlalchemy-utils==0.41.2
Expand Down
7 changes: 5 additions & 2 deletions backend/src/appointment/controller/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import hashlib
import hmac
import datetime
import urllib.parse

from sqlalchemy.orm import Session

Expand Down Expand Up @@ -47,11 +48,13 @@ def signed_url_by_subscriber(subscriber: schemas.Subscriber):
if not short_url:
short_url = base_url

url_safe_username = urllib.parse.quote_plus(subscriber.username)

# We sign with a different hash that the end-user doesn't have access to
# We also need to use the default url, as short urls are currently setup as a redirect
url = f'{base_url}/{subscriber.username}/{subscriber.short_link_hash}'
url = f'{base_url}/{url_safe_username}/{subscriber.short_link_hash}'

signature = sign_url(url)

# We return with the signed url signature
return f'{short_url}/{subscriber.username}/{signature}'
return f'{short_url}/{url_safe_username}/{signature}'
7 changes: 7 additions & 0 deletions backend/src/appointment/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class Subscriber(HasSoftDelete, Base):
# Only accept the times greater than the one specified in the `iat` claim of the jwt token
minimum_valid_iat_time = Column('minimum_valid_iat_time', encrypted_type(DateTime))

ftue_level = Column(Integer, nullable=False, default=0, index=True)

calendars = relationship('Calendar', cascade='all,delete', back_populates='owner')
slots = relationship('Slot', cascade='all,delete', back_populates='subscriber')
external_connections = relationship('ExternalConnections', cascade='all,delete', back_populates='owner')
Expand All @@ -153,6 +155,11 @@ def preferred_email(self):
"""Returns the preferred email address."""
return self.secondary_email if self.secondary_email is not None else self.email

@property
def is_setup(self) -> bool:
"""Has the user been through the First Time User Experience?"""
return self.ftue_level > 0


class Calendar(Base):
__tablename__ = 'calendars'
Expand Down
3 changes: 3 additions & 0 deletions backend/src/appointment/database/repo/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import re
import datetime
import urllib.parse

from sqlalchemy.orm import Session
from .. import models, schemas
Expand Down Expand Up @@ -125,6 +126,8 @@ def verify_link(db: Session, url: str):
"""
username, signature, clean_url = utils.retrieve_user_url_data(url)

username = urllib.parse.unquote_plus(username)

subscriber = get_by_username(db, username)
if not subscriber:
return False
Expand Down
10 changes: 6 additions & 4 deletions backend/src/appointment/database/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class Config:

class ScheduleBase(BaseModel):
active: bool | None = True
name: str = Field(min_length=1)
name: str = Field(min_length=1, max_length=128)
slug: Optional[str] = None
calendar_id: int
location_type: LocationType | None = LocationType.inperson
Expand Down Expand Up @@ -250,15 +250,16 @@ class Invite(BaseModel):

class SubscriberIn(BaseModel):
timezone: str | None = None
username: str
name: str | None = None
username: str = Field(min_length=1, max_length=128)
name: Optional[str] = Field(min_length=1, max_length=128, default=None)
avatar_url: str | None = None
secondary_email: str | None = None


class SubscriberBase(SubscriberIn):
email: str
email: str = Field(min_length=1, max_length=200)
preferred_email: str | None = None
is_setup: bool | None = None
level: SubscriberLevel | None = SubscriberLevel.basic


Expand All @@ -270,6 +271,7 @@ class Subscriber(SubscriberAuth):
id: int
calendars: list[Calendar] = []
slots: list[Slot] = []
ftue_level: Optional[int] = Field(gte=0)

class Config:
from_attributes = True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""add ftue_level to subscribers
Revision ID: 156b3b0d77b9
Revises: 12c7e1b34dd6
Create Date: 2024-06-25 21:33:27.094632
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '156b3b0d77b9'
down_revision = '12c7e1b34dd6'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column('subscribers', sa.Column('ftue_level', sa.Integer, default=0, nullable=False, index=True))


def downgrade() -> None:
op.drop_column('subscribers', 'ftue_level')
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""empty message
Revision ID: 0c22678e25db
Revises: 156b3b0d77b9, a9ca5a4325ec
Create Date: 2024-07-02 16:36:47.372956
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '0c22678e25db'
down_revision = ('156b3b0d77b9', 'a9ca5a4325ec')
branch_labels = None
depends_on = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass
1 change: 1 addition & 0 deletions backend/src/appointment/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def update_me(
name=me.name,
level=me.level,
timezone=me.timezone,
is_setup=me.is_setup,
)


Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def me(
level=subscriber.level,
timezone=subscriber.timezone,
avatar_url=subscriber.avatar_url,
is_setup=subscriber.is_setup,
)


Expand Down
10 changes: 9 additions & 1 deletion backend/src/appointment/routes/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ def update_schedule(
and subscriber.get_external_connection(ExternalConnectionType.zoom) is None
):
raise validation.ZoomNotConnectedException()

if schedule.slug is None:
# If slug isn't provided, give them the last 8 characters from a uuid4
schedule.slug = repo.schedule.generate_slug(db, id)
if not schedule.slug:
# A little extra, but things are a little out of place right now..
raise validation.ScheduleCreationException()

return repo.schedule.update(db=db, schedule=schedule, schedule_id=id)


Expand Down Expand Up @@ -435,7 +443,7 @@ def decide_on_schedule_availability_slot(
title=title,
start=slot.start.replace(tzinfo=timezone.utc),
end=slot.start.replace(tzinfo=timezone.utc) + timedelta(minutes=slot.duration),
description=schedule.details,
description=schedule.details or '',
location=schemas.EventLocation(
type=schedule.location_type,
url=location_url,
Expand Down
18 changes: 17 additions & 1 deletion backend/src/appointment/routes/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

from ..database import repo, schemas, models
from ..database.models import Subscriber
from ..dependencies.auth import get_admin_subscriber
from ..dependencies.auth import get_admin_subscriber, get_subscriber
from ..dependencies.database import get_db

from ..exceptions import validation

router = APIRouter()

""" ADMIN ROUTES
These require get_admin_subscriber!
"""

@router.get('/', response_model=list[schemas.SubscriberAdminOut])
def get_all_subscriber(db: Session = Depends(get_db), _: Subscriber = Depends(get_admin_subscriber)):
Expand Down Expand Up @@ -47,3 +50,16 @@ def enable_subscriber(email: str, db: Session = Depends(get_db), _: Subscriber =

# Set active flag to true on the subscribers model.
return repo.subscriber.enable(db, subscriber_to_enable)


""" NON-ADMIN ROUTES """


@router.post('/setup')
def subscriber_is_setup(db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)):
"""Flips ftue_level to 1"""
subscriber.ftue_level = 1
db.add(subscriber)
db.commit()

return True
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ services:
build: ./frontend
volumes:
- './frontend:/app'
- '/app/node_modules'
- 'node_modules:/app/node_modules'
ports:
- 8080:8080
environment:
Expand Down Expand Up @@ -54,3 +54,4 @@ services:
volumes:
db: {}
cache: {}
node_modules: {}
15 changes: 15 additions & 0 deletions frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ module.exports = {
browser: true,
es2021: true,
},
parser: '@typescript-eslint/parser',
extends: [
'plugin:vue/vue3-essential',
'plugin:tailwindcss/recommended',
'plugin:@typescript-eslint/recommended',
'airbnb-base',
],
overrides: [],
Expand All @@ -18,6 +20,7 @@ module.exports = {
},
plugins: [
'vue',
'@typescript-eslint',
],
rules: {
'import/extensions': ['error', 'ignorePackages', {
Expand All @@ -31,6 +34,18 @@ module.exports = {
'tailwindcss/no-custom-classname': 'off',
'import/prefer-default-export': 'off',
radix: 'off',
'@typescript-eslint/no-explicit-any': 'off',
// Disable full warning, and customize the typescript one
// Warn about unused vars unless they start with an underscore
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
settings: {
'import/resolver': {
Expand Down
13 changes: 13 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ yarn run serve
yarn run build
```

### Post-CSS

We use post-css to enhance our css. Any post-css that isn't in a SFC must be in a `.pcss` file and imported into the scoped style like so:
```css
@import '@/assets/styles/custom-media.pcss';

@media (--md) {
.container {
...
}
}
```

### Lints and fixes files

Frontend is formatted using ESlint with airbnb rules.
Expand Down
6 changes: 4 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thunderbird Appointment</title>
<script>
const currentPath = new URL(location.href).pathname;
// handle theme color scheme
if (
localStorage?.getItem('theme') === 'dark'
|| (!localStorage?.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)
!currentPath.startsWith('/setup')
&& (localStorage?.getItem('theme') === 'dark'
|| (!localStorage?.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches))
) {
document.documentElement.classList.add('dark');
} else {
Expand Down
11 changes: 7 additions & 4 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"autoprefixer": "^10.4.12",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
Expand All @@ -44,19 +46,20 @@
"node-fetch": "^3.3.2",
"postcss": "^8.4.17",
"postcss-nesting": "^12.0.3",
"typescript": "^5.4.5",
"typescript": "^5.5.3",
"vite-plugin-eslint": "^1.8.1",
"vitest": "^1.1.0"
},
"postcss": {
"plugins": {
"tailwindcss": {},
"autoprefixer": {},
"postcss-nesting": {}
"postcss-nesting": {},
"postcss-custom-media": {}
}
},
"type": "module",
"engines": {
"node": ">=20.15.0"
}
},
"type": "module"
}
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ const scheduleStore = useScheduleStore();
// true if route can be accessed without authentication
const routeIsPublic = computed(
() => ['availability', 'home', 'login', 'post-login', 'confirmation', 'terms', 'privacy', 'waiting-list'].includes(route.name),
() => route.meta?.isPublic,
);
const routeIsHome = computed(
() => ['home'].includes(route.name),
Expand Down
Binary file not shown.
Binary file not shown.
Binary file added frontend/src/assets/fonts/Inter/Inter-Bold.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added frontend/src/assets/fonts/Inter/Inter-Thin.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
24 changes: 24 additions & 0 deletions frontend/src/assets/fonts/Metropolis/UNLICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>
Loading

0 comments on commit 6f58211

Please sign in to comment.