Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 104 additions & 44 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,61 +1,121 @@
name: Deploy Schej
name: Deploy Timeful

on:
push:
branches:
- main
workflow_dispatch:
inputs:
target:
description: "What to deploy?"
required: true
type: choice
options: [server, frontend, both]

permissions:
contents: read
id-token: write

concurrency:
group: timeful-production
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

# -----------------------------
# Frontend
# -----------------------------

- name: Set up Node
uses: actions/setup-node@v4
with:
ref: ${{ github.ref_name }}
node-version: "18"
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Prepare SSH
shell: bash
- name: Build frontend
working-directory: frontend
env:
VUE_APP_GOOGLE_CLIENT_ID: ${{ vars.GOOGLE_CLIENT_ID }}
VUE_APP_POSTHOG_API_KEY: ""
VUE_APP_MICROSOFT_CLIENT_ID: ""
run: |
set -euo pipefail
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > aws-key
chmod 600 aws-key
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
npm ci
npm run build

- name: Ensure deploy scripts are executable
run: chmod +x ./deploy_scripts/*.sh
# -----------------------------
# Backend
# -----------------------------

- name: Deploy
shell: bash
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: server/go.sum

- name: Build backend
working-directory: server
env:
TARGET: ${{ github.event.inputs.target }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
CGO_ENABLED: "0"
GOOS: linux
GOARCH: amd64
run: |
go build \
-buildvcs=false \
-trimpath \
-ldflags="-s -w" \
-o ../timeful-server \
.

# -----------------------------
# Release archive
# -----------------------------

- name: Package release
run: |
mkdir -p release/frontend

cp timeful-server release/timeful-server
cp -a frontend/dist release/frontend/dist

chmod +x release/timeful-server

tar -C release -czf timeful-release.tar.gz .

# -----------------------------
# Authenticate to Google Cloud
# -----------------------------

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
project_id: ${{ vars.GCP_PROJECT_ID }}
workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}

- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3

# -----------------------------
# Deploy through IAP
# -----------------------------

- name: Upload release
run: |
gcloud compute scp \
timeful-release.tar.gz \
"${{ vars.GCP_VM }}:/tmp/timeful-release.tar.gz" \
--project="${{ vars.GCP_PROJECT_ID }}" \
--zone="${{ vars.GCP_ZONE }}" \
--tunnel-through-iap \
--quiet

- name: Activate release
run: |
set -euo pipefail
DEPLOY_SCRIPT_DIRECTORY="./deploy_scripts/"
HOST="${SSH_USER}@${SSH_HOST}"

if [ "$TARGET" = "server" ]; then
DEPLOY_SCRIPT="deploy_server.sh"
elif [ "$TARGET" = "frontend" ]; then
DEPLOY_SCRIPT="deploy_frontend.sh"
elif [ "$TARGET" = "both" ]; then
for DEPLOY_SCRIPT in deploy_frontend.sh deploy_server.sh; do
echo "${DEPLOY_SCRIPT_DIRECTORY}${DEPLOY_SCRIPT}"
/bin/bash "${DEPLOY_SCRIPT_DIRECTORY}${DEPLOY_SCRIPT}" "$HOST" "./aws-key"
done
exit 0
else
echo "Invalid script name!"
exit 1
fi

echo "${DEPLOY_SCRIPT_DIRECTORY}${DEPLOY_SCRIPT}"
/bin/bash "${DEPLOY_SCRIPT_DIRECTORY}${DEPLOY_SCRIPT}" "$HOST" "./aws-key"
gcloud compute ssh \
"${{ vars.GCP_VM }}" \
--project="${{ vars.GCP_PROJECT_ID }}" \
--zone="${{ vars.GCP_ZONE }}" \
--tunnel-through-iap \
--quiet \
--command="sudo /usr/local/sbin/deploy-timeful /tmp/timeful-release.tar.gz && rm -f /tmp/timeful-release.tar.gz"
12 changes: 12 additions & 0 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Applies dark mode before first paint to avoid a flash of the wrong theme -->
<script>
;(function (d) {
var stored = localStorage["darkMode"]
var dark =
stored == undefined
? window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
: stored == "true"
if (dark) d.documentElement.classList.add("dark")
})(document)
</script>
<!-- Google Tag Manager -->
<script>
;(function (w, d, s, l, i) {
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
>
Donate
</v-btn> -->
<v-btn
id="dark-mode-toggle-btn"
icon
:aria-label="darkMode ? 'Switch to light mode' : 'Switch to dark mode'"
@click="toggleDarkMode"
>
<v-icon>{{
darkMode ? "mdi-white-balance-sunny" : "mdi-weather-night"
}}</v-icon>
</v-btn>
<v-btn
v-if="$route.name === 'home' && !isPhone"
color="primary"
Expand Down Expand Up @@ -238,6 +248,7 @@ import {
signInGoogle,
signInOutlook,
isPremiumUser,
setDarkModePreference,
} from "@/utils"
import {
authTypes,
Expand Down Expand Up @@ -295,6 +306,7 @@ export default {
"enablePaywall",
"upgradeDialogVisible",
"newDialogOptions",
"darkMode",
]),
isPhone() {
return isPhone(this.$vuetify)
Expand Down Expand Up @@ -331,6 +343,7 @@ export default {
"setPricingPageConversion",
"setEnablePaywall",
"setFeatureFlagsLoaded",
"setDarkMode",
]),
...mapActions([
"getEvents",
Expand All @@ -341,6 +354,12 @@ export default {
handleScroll(e) {
this.scrollY = window.scrollY
},
toggleDarkMode() {
const darkMode = !this.darkMode
this.setDarkMode(darkMode)
setDarkModePreference(darkMode)
this.$posthog?.capture("dark_mode_toggled", { darkMode })
},
_createNew(eventOnly = false) {
this.$posthog.capture("create_new_button_clicked", {
eventOnly: eventOnly,
Expand Down Expand Up @@ -460,6 +479,13 @@ export default {
},

watch: {
darkMode: {
immediate: true,
handler(darkMode) {
document.documentElement.classList.toggle("dark", darkMode)
this.$vuetify.theme.dark = darkMode
},
},
$route: {
immediate: true,
async handler() {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/AuthUserMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
target="_blank"
>
<v-list-item-title class="tw-flex tw-items-center tw-gap-1">
<v-icon class="tw-mr-1" small color="black">mdi-message</v-icon>
<v-icon class="tw-mr-1" small>mdi-message</v-icon>
Give feedback
</v-list-item-title>
</v-list-item>
<v-list-item id="settings-btn" @click="goToSettings">
<v-list-item-title class="tw-flex tw-items-center tw-gap-1">
<v-icon class="tw-mr-1" small color="black">mdi-cog</v-icon>
<v-icon class="tw-mr-1" small>mdi-cog</v-icon>
Settings
</v-list-item-title>
</v-list-item>
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
:root {
--color-white: 255 255 255;
--color-off-white: 242 242 242;
--color-black: 0 0 0;
--color-gray: 189 189 189;
--color-dark-gray: 107 107 107;
--color-very-dark-gray: 79 79 79;
--color-light-gray: 243 244 246;
--color-light-gray-stroke: 223 223 223;
}

html.dark {
--color-white: 30 30 30;
--color-off-white: 42 42 42;
--color-black: 242 242 242;
--color-gray: 92 92 92;
--color-dark-gray: 168 168 168;
--color-very-dark-gray: 208 208 208;
--color-light-gray: 38 38 38;
--color-light-gray-stroke: 58 58 58;
}

html,
body {
background-color: rgb(var(--color-white));
}

@tailwind base;
@tailwind components;
@tailwind utilities;
8 changes: 8 additions & 0 deletions frontend/src/plugins/vuetify.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import Vue from "vue"
import Vuetify from "vuetify/lib"
import tailwind from "../../tailwind.config"
import { prefersDarkMode } from "@/utils/general_utils"

Vue.use(Vuetify)

export default new Vuetify({
theme: {
dark: prefersDarkMode(),
themes: {
light: {
primary: tailwind.theme.colors.green,
error: tailwind.theme.colors.red,
},
dark: {
primary: tailwind.theme.colors["light-green"],
error: tailwind.theme.colors.red,
background: "#1e1e1e",
surface: "#1e1e1e",
},
},
},
breakpoint: {
Expand Down
28 changes: 10 additions & 18 deletions frontend/src/store/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Vue from "vue"
import Vuex from "vuex"
import { numFreeEvents, upgradeDialogTypes } from "@/constants"
import { get, isPremiumUser } from "@/utils"
import { upgradeDialogTypes } from "@/constants"
import { get, isPremiumUser, prefersDarkMode } from "@/utils"
import {
createFolder,
deleteFolder,
Expand All @@ -19,6 +19,8 @@ export default new Vuex.Store({

authUser: null,

darkMode: prefersDarkMode(),

events: [],
folders: [],

Expand All @@ -29,7 +31,7 @@ export default new Vuex.Store({
signUpFormEnabled: false,
daysOnlyEnabled: true,
overlayAvailabilitiesEnabled: true,
enablePaywall: true,
enablePaywall: false,

// Experiments
pricingPageConversion: "control",
Expand Down Expand Up @@ -61,6 +63,10 @@ export default new Vuex.Store({
state.info = info
},

setDarkMode(state, darkMode) {
state.darkMode = darkMode
},

setAuthUser(state, authUser) {
state.authUser = authUser
},
Expand Down Expand Up @@ -163,21 +169,7 @@ export default new Vuex.Store({
commit("setAuthUser", authUser)
},

createNew(
{ state, getters, commit, dispatch },
{ eventOnly = false, folderId = null }
) {
if (
state.enablePaywall &&
!getters.isPremiumUser &&
state.authUser?.numEventsCreated >= numFreeEvents
) {
dispatch("showUpgradeDialog", {
type: upgradeDialogTypes.CREATE_EVENT,
})
return
}

createNew({ commit }, { eventOnly = false, folderId = null }) {
commit("setNewDialogOptions", {
show: true,
contactsPayload: {},
Expand Down
Loading