├── src
├── hooks
│ ├── index.ts
│ └── useIsOnline.tsx
├── sql
│ ├── migration.0-to-1.sql
│ ├── schema.0.sqlite.sql
│ ├── schema.1.sqlite.sql
│ ├── schema.2.sqlite.sql
│ └── migration.1-to-2.sql
├── types
│ ├── prompts.ts
│ ├── index.ts
│ ├── entries.ts
│ ├── tags.ts
│ └── subscriptions.ts
├── context
│ └── index.ts
├── config
│ ├── autoformat
│ │ ├── index.ts
│ │ ├── autoformatConstants.ts
│ │ ├── autoformatRules.ts
│ │ ├── autoformatOperation.ts
│ │ ├── autoformatUtils.ts
│ │ ├── autoformatLists.ts
│ │ ├── autoformatMarks.ts
│ │ └── autoformatBlocks.ts
│ ├── index.ts
│ ├── resetBlockTypePlugin.ts
│ └── UserPreferences.ts
├── utils
│ ├── breakpoints.ts
│ ├── logger.ts
│ ├── index.ts
│ ├── dates.ts
│ ├── supabaseClient.ts
│ ├── crypto.ts
│ └── misc.ts
├── constants
│ └── index.ts
├── index.html
├── components
│ ├── Menu
│ │ ├── Settings
│ │ │ ├── Billing
│ │ │ │ ├── types.ts
│ │ │ │ ├── Balance.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── Receipts.tsx
│ │ │ │ ├── PaymentMethod.tsx
│ │ │ │ └── styled.tsx
│ │ │ ├── Earn
│ │ │ │ └── index.tsx
│ │ │ ├── Subscribe
│ │ │ │ ├── LeftPanel.tsx
│ │ │ │ ├── styled.tsx
│ │ │ │ ├── Success.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Upgrade
│ │ │ │ ├── index.tsx
│ │ │ │ └── Features.tsx
│ │ │ ├── ChangeCycle
│ │ │ │ ├── LeftPanel.tsx
│ │ │ │ ├── Success.tsx
│ │ │ │ ├── styled.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── ImportExport
│ │ │ │ ├── nodeTypes.ts
│ │ │ │ └── index.tsx
│ │ │ ├── AddCard
│ │ │ │ ├── styled.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── CancelImmediately
│ │ │ │ └── styled.tsx
│ │ │ └── CancelOrResume
│ │ │ │ └── styled.tsx
│ │ └── AppearanceToolbar
│ │ │ └── styled.tsx
│ ├── Icon
│ │ ├── Plus.tsx
│ │ ├── Menu.tsx
│ │ ├── FormatMark.tsx
│ │ ├── Trash.tsx
│ │ ├── Edit.tsx
│ │ ├── TrafficLightOutline.tsx
│ │ ├── FormatItalic.tsx
│ │ ├── Offline.tsx
│ │ ├── FormatCode.tsx
│ │ ├── BlockH1.tsx
│ │ ├── FormatUnderline.tsx
│ │ ├── UpdateNow.tsx
│ │ ├── BlockBulletList.tsx
│ │ ├── FormatBold.tsx
│ │ ├── BlockH2.tsx
│ │ ├── BlockText.tsx
│ │ ├── TrafficLightCalendar.tsx
│ │ ├── FormatStriketrough.tsx
│ │ ├── Cross.tsx
│ │ ├── BlockH3.tsx
│ │ ├── BlockNumList.tsx
│ │ ├── Exit.tsx
│ │ ├── Settings.tsx
│ │ ├── FormatHandStriketrough.tsx
│ │ ├── Bucket.tsx
│ │ ├── Check.tsx
│ │ ├── Chevron.tsx
│ │ └── index.tsx
│ ├── index.ts
│ ├── EntryList
│ │ ├── styled.ts
│ │ └── index.tsx
│ ├── Splash.tsx
│ ├── Entry
│ │ ├── LimitReached.tsx
│ │ ├── styled.ts
│ │ ├── EntryMenu
│ │ │ ├── Modal.tsx
│ │ │ └── styled.tsx
│ │ └── EntryAside.tsx
│ ├── FadeOut.tsx
│ ├── TrafficLightMenu.tsx
│ ├── FormatToolbar
│ │ └── BlockTypeSelectItem.tsx
│ ├── ScrollToToday.tsx
│ ├── FeedbackWidget
│ │ └── RatingEmojiControl.tsx
│ ├── EntryTags
│ │ └── ListItemTagColorPicker.tsx
│ ├── ConfirmationModal.tsx
│ └── Login.tsx
├── renderer.tsx
├── services
│ ├── mdx.ts
│ ├── saveFile.ts
│ ├── autoUpdater.ts
│ └── analytics.ts
├── themes
│ └── index.ts
└── index.css
├── decs.d.ts
├── .vscode
└── settings.json
├── assets
├── background.tiff
├── dmg-background.png
├── dmg-background@2x.png
├── fonts
│ ├── Inter.var.woff2
│ ├── novela-bold-webfont.woff2
│ ├── novela-regular-webfont.woff2
│ ├── novela-bolditalic-webfont.woff2
│ └── novela-regularitalic-webfont.woff2
├── icons
│ ├── journaldo-logo@2x.png
│ └── journal-macos-icon.icns
└── images
│ ├── beginning-dark@2x.png
│ ├── beginning-forest@2x.png
│ ├── beginning-light@2x.png
│ └── beginning-cappuccino@2x.png
├── webpack.plugins.js
├── dist
└── main_window
│ └── index.html
├── playwright.config.ts
├── entitlements.plist
├── tests
└── helpers
│ ├── utils.ts
│ ├── sqlite.ts
│ └── supabase.ts
├── README.md
├── webpack.main.config.js
├── .eslintrc.json
├── webpack.renderer.config.js
├── tsconfig.json
├── webpack.rules.js
├── .gitignore
├── schemas
├── schema.postgresql.sql
└── migration2.postgres.sql
└── updates
└── darwin
├── x64
└── update.json
└── arm64
└── update.json
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useIsOnline'
2 |
--------------------------------------------------------------------------------
/decs.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png'
2 | declare module '*.sql'
3 |
--------------------------------------------------------------------------------
/src/sql/migration.0-to-1.sql:
--------------------------------------------------------------------------------
1 | alter table users
2 | add secret_key blob;
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.semi": false,
3 | "editor.formatOnSave": true
4 | }
--------------------------------------------------------------------------------
/assets/background.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/background.tiff
--------------------------------------------------------------------------------
/assets/dmg-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/dmg-background.png
--------------------------------------------------------------------------------
/assets/dmg-background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/dmg-background@2x.png
--------------------------------------------------------------------------------
/assets/fonts/Inter.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/fonts/Inter.var.woff2
--------------------------------------------------------------------------------
/assets/icons/journaldo-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/icons/journaldo-logo@2x.png
--------------------------------------------------------------------------------
/assets/icons/journal-macos-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/icons/journal-macos-icon.icns
--------------------------------------------------------------------------------
/assets/images/beginning-dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/images/beginning-dark@2x.png
--------------------------------------------------------------------------------
/assets/images/beginning-forest@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/images/beginning-forest@2x.png
--------------------------------------------------------------------------------
/assets/images/beginning-light@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/images/beginning-light@2x.png
--------------------------------------------------------------------------------
/assets/fonts/novela-bold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/fonts/novela-bold-webfont.woff2
--------------------------------------------------------------------------------
/src/types/prompts.ts:
--------------------------------------------------------------------------------
1 | type Prompt = {
2 | id: number
3 | title: string
4 | content: any
5 | }
6 |
7 | export { Prompt }
8 |
--------------------------------------------------------------------------------
/assets/fonts/novela-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/fonts/novela-regular-webfont.woff2
--------------------------------------------------------------------------------
/assets/images/beginning-cappuccino@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/images/beginning-cappuccino@2x.png
--------------------------------------------------------------------------------
/src/context/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AppearanceContext'
2 | export * from './EntriesContext/index'
3 | export * from './UserContext'
4 |
--------------------------------------------------------------------------------
/assets/fonts/novela-bolditalic-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/fonts/novela-bolditalic-webfont.woff2
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './subscriptions'
2 | export * from './tags'
3 | export * from './entries'
4 | export * from './prompts'
5 |
--------------------------------------------------------------------------------
/assets/fonts/novela-regularitalic-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JournalApp/Journal/HEAD/assets/fonts/novela-regularitalic-webfont.woff2
--------------------------------------------------------------------------------
/src/config/autoformat/index.ts:
--------------------------------------------------------------------------------
1 | export * from './autoformatBlocks'
2 | export * from './autoformatUtils'
3 | export * from './autoformatRules'
4 |
--------------------------------------------------------------------------------
/webpack.plugins.js:
--------------------------------------------------------------------------------
1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
2 |
3 | module.exports = [
4 | new ForkTsCheckerWebpackPlugin()
5 | ];
6 |
--------------------------------------------------------------------------------
/src/utils/breakpoints.ts:
--------------------------------------------------------------------------------
1 | const breakpoints = {
2 | s: '(max-width: 600px)',
3 | xs: '(max-width: 480px)',
4 | xl: '(min-width: 1560px)',
5 | }
6 |
7 | export { breakpoints }
8 |
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | import { isDev } from './misc'
2 |
3 | const logger = (data: any) => {
4 | if (isDev()) {
5 | console.log(data)
6 | }
7 | }
8 |
9 | export { logger }
10 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatConstants.ts:
--------------------------------------------------------------------------------
1 | export const DIGITS: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']
2 |
3 | export const DIGITS_WITH_SPACE: string[] = DIGITS.map((digit) => ` ${digit}`)
4 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './misc'
2 | export * from './dates'
3 | export * from './supabaseClient'
4 | export * from './breakpoints'
5 | export * from './crypto'
6 | export * from './logger'
7 | export * from './zipCodes'
8 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { isDev } from '../utils'
2 |
3 | export const productWriterId = isDev() ? 'prod_MaCerQCQryctSx' : 'prod_Mz9pjbqvffW1in'
4 | export const entriesLimit = 30
5 | export const betaEndDate = isDev() ? '2021-10-05' : '2022-12-15'
6 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Journal
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Billing/types.ts:
--------------------------------------------------------------------------------
1 | import Stripe from 'stripe'
2 | import { BillingInfo } from 'types'
3 |
4 | interface PaymentMethodProps {
5 | billingInfo: BillingInfo
6 | isLoading: boolean
7 | showCardOnly?: boolean
8 | }
9 |
10 | export { PaymentMethodProps }
11 |
--------------------------------------------------------------------------------
/src/renderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import './index.css'
4 |
5 | import { App } from './App'
6 |
7 | function renderApp() {
8 | render(, document.getElementById('app'))
9 | }
10 |
11 | document.fonts.load('12px "Inter var"').then(() => renderApp())
12 |
--------------------------------------------------------------------------------
/dist/main_window/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hello World!
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { PlaywrightTestConfig } from '@playwright/test'
2 |
3 | const config: PlaywrightTestConfig = {
4 | testDir: './tests',
5 | reporter: [['list', { printSteps: true }]],
6 | timeout: 60000,
7 |
8 | expect: {
9 | toMatchSnapshot: { threshold: 0.2 },
10 | },
11 | }
12 |
13 | export default config
14 |
--------------------------------------------------------------------------------
/entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.debugger
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Earn/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled from 'styled-components'
3 | import { theme } from 'themes'
4 | import { SectionTitleStyled } from '../styled'
5 |
6 | const EarnTabContent = () => {
7 | return Earn credit
8 | }
9 |
10 | export { EarnTabContent }
11 |
--------------------------------------------------------------------------------
/tests/helpers/utils.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 |
3 | export function getRecordingPath(platform: string, fileName?: string) {
4 | const paths = ['test-output', platform, 'screenshots']
5 | if (fileName) {
6 | paths.push(fileName)
7 | }
8 | return path.join(...paths)
9 | }
10 |
11 | export async function pause(ms: number) {
12 | return new Promise((f) => setTimeout(f, ms))
13 | }
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🙋 The Journal app is looking for a maintainer who will help in transforming it into an open-source app
2 |
3 | What needs to be done:
4 | - Remove Supabase and maintain local SQLite only
5 | - Remove Stripe payments
6 | - Migrate users' encryption keys
7 | - Create a release
8 | - Make users happy
9 |
10 | Contact me at [support@journal.do](mailto:support@journal.do) or [@jarekceborski](https://x.com/jarekceborski)
11 |
--------------------------------------------------------------------------------
/src/services/mdx.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain } from 'electron'
2 | import { logger, isDev } from '../utils'
3 | import { serialize } from 'next-mdx-remote/serialize'
4 |
5 | ipcMain.handle('mdx-serialize', async (event, source: string) => {
6 | logger('mdx-serialize')
7 | try {
8 | return await serialize(source)
9 | } catch (error) {
10 | logger(`error`)
11 | logger(error)
12 | return await serialize('')
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/src/types/entries.ts:
--------------------------------------------------------------------------------
1 | type SyncStatus = 'synced' | 'pending_insert' | 'pending_update' | 'pending_delete'
2 |
3 | type Day = `${number}-${number}-${number}`
4 |
5 | type Entry = {
6 | user_id: string
7 | day: Day
8 | journal_id?: number
9 | created_at: string
10 | modified_at: string
11 | content: any[] | string
12 | iv?: string
13 | revision?: number
14 | sync_status?: SyncStatus
15 | }
16 |
17 | export { Day, Entry }
18 |
--------------------------------------------------------------------------------
/webpack.main.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /**
3 | * This is the main entry point for your application, it's the first file
4 | * that runs in the main process.
5 | */
6 | entry: './src/index.ts',
7 | // Put your normal webpack config below here
8 | target: 'electron-main',
9 | module: {
10 | rules: require('./webpack.rules'),
11 | },
12 | resolve: {
13 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json', '.sql'],
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Icon/Plus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Plus({ tintColor, ...props }: any) {
5 | return (
6 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "rules": {
8 | "semi": "off",
9 | "@typescript-eslint/semi": ["error"]
10 | },
11 | "extends": [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/eslint-recommended",
14 | "plugin:@typescript-eslint/recommended",
15 | "plugin:import/recommended",
16 | "plugin:import/electron",
17 | "plugin:import/typescript"
18 | ],
19 | "parser": "@typescript-eslint/parser"
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Icon/Menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Menu({ tintColor, ...props }: any) {
5 | return (
6 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatMark.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatMark({ tintColor, ...props }: any) {
5 | return (
6 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Entry'
2 | export * from './EntryTags'
3 | export * from './Icon'
4 | export * from './Calendar'
5 | export * from './FormatToolbar'
6 | export * from './EntryList'
7 | export * from './ContextMenu'
8 | export * from './Menu'
9 | export * from './TrafficLightMenu'
10 | export * from './FadeOut'
11 | export * from './ScrollToToday'
12 | export * from './Login'
13 | export * from './FeedbackWidget'
14 | export * from './Splash'
15 | export * from './Menu/Settings'
16 | export * from './ConfirmationModal'
17 | export * from './Prompts'
18 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { AutoformatPlugin, PlatePlugin } from '@udecode/plate'
2 | import { autoformatRules } from './autoformat/autoformatRules'
3 |
4 | interface Config {
5 | autoformat: Partial>
6 | }
7 |
8 | export const defaultContent = [
9 | {
10 | children: [
11 | {
12 | text: '',
13 | },
14 | ],
15 | },
16 | ]
17 |
18 | export const CONFIG: Config = {
19 | autoformat: {
20 | options: {
21 | rules: autoformatRules,
22 | },
23 | },
24 | }
25 |
26 | export * from './UserPreferences'
27 |
--------------------------------------------------------------------------------
/src/components/Icon/Trash.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Trash({ tintColor, ...props }: any) {
5 | return (
6 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/webpack.renderer.config.js:
--------------------------------------------------------------------------------
1 | const rules = require('./webpack.rules')
2 | const plugins = require('./webpack.plugins')
3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
4 |
5 | rules.push({
6 | test: /\.css$/,
7 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
8 | })
9 |
10 | module.exports = {
11 | module: {
12 | rules,
13 | },
14 | // target: 'electron-renderer',
15 | plugins: plugins,
16 | resolve: {
17 | plugins: [new TsconfigPathsPlugin()],
18 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Icon/Edit.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Edit({ tintColor, size, ...props }: any) {
5 | switch (size) {
6 | default:
7 | return (
8 |
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Icon/TrafficLightOutline.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function TrafficLightOutline({ tintColor, ...props }: any) {
5 | return (
6 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useIsOnline.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | function useIsOnline() {
4 | const [isOnline, setIsOnline] = useState(navigator.onLine)
5 |
6 | const updateIsOnline = () => {
7 | setIsOnline(navigator.onLine)
8 | }
9 |
10 | useEffect(() => {
11 | window.addEventListener('online', updateIsOnline)
12 | window.addEventListener('offline', updateIsOnline)
13 | return () => {
14 | window.removeEventListener('online', updateIsOnline)
15 | window.removeEventListener('offline', updateIsOnline)
16 | }
17 | })
18 |
19 | return isOnline
20 | }
21 |
22 | export { useIsOnline }
23 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatItalic.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatItalic({ tintColor, ...props }: any) {
5 | return (
6 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Icon/Offline.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Offline({ tintColor, ...props }: any) {
5 | return (
6 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatCode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatCode({ tintColor, ...props }: any) {
5 | return (
6 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Icon/BlockH1.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function BlockH1({ tintColor, ...props }: any) {
5 | return (
6 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/EntryList/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { theme } from 'themes'
3 |
4 | const BeforeEntries = styled.div`
5 | text-align: center;
6 | margin: 48px 0 24px 0;
7 | height: 70px;
8 | background: ${theme('style.beginningImage')} no-repeat top center;
9 | background-size: 300px 70px;
10 | `
11 |
12 | const PostEntries = styled.div`
13 | min-height: calc(100vh - 150px);
14 | `
15 |
16 | const Wrapper = styled.div`
17 | width: 100vw;
18 | margin-left: ${theme('appearance.entriesOffset')};
19 | transition: margin-left ${theme('animation.time.normal')};
20 | display: flex;
21 | flex-flow: column;
22 | flex-direction: column-reverse;
23 | `
24 |
25 | export { BeforeEntries, PostEntries, Wrapper }
26 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Subscribe/LeftPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 |
5 | const LeftPanelStyled = styled.div`
6 | width: 260px;
7 | color: ${theme('color.primary.main')};
8 | font-weight: 500;
9 | font-size: 21px;
10 | line-height: 26px;
11 | & em {
12 | opacity: 0.6;
13 | font-style: normal;
14 | }
15 | `
16 |
17 | const LeftPanel = () => {
18 | return (
19 |
20 | Upgrade,
21 |
22 |
23 | write
24 |
25 | without
26 |
27 | limits
28 |
29 |
30 | )
31 | }
32 |
33 | export { LeftPanel }
34 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatRules.ts:
--------------------------------------------------------------------------------
1 | import {
2 | autoformatArrow,
3 | autoformatLegal,
4 | autoformatLegalHtml,
5 | autoformatMath,
6 | autoformatPunctuation,
7 | autoformatSmartQuotes,
8 | } from '@udecode/plate'
9 | import { autoformatBlocks } from './autoformatBlocks'
10 | import { autoformatLists } from './autoformatLists'
11 | import { autoformatMarks } from './autoformatMarks'
12 | import { autoformatMultiplication } from './autoformatOperation'
13 |
14 | export const autoformatRules = [
15 | ...autoformatBlocks,
16 | ...autoformatLists,
17 | ...autoformatMarks,
18 | ...autoformatMultiplication,
19 | ...autoformatSmartQuotes,
20 | ...autoformatPunctuation,
21 | ...autoformatLegal,
22 | ...autoformatLegalHtml,
23 | ...autoformatArrow,
24 | ...autoformatMath,
25 | ]
26 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatUnderline.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatUnderline({ tintColor, ...props }: any) {
5 | return (
6 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Icon/UpdateNow.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export function UpdateNow({ tintColor, ...props }: any) {
4 | return (
5 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/config/resetBlockTypePlugin.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ELEMENT_BLOCKQUOTE,
3 | ELEMENT_PARAGRAPH,
4 | ELEMENT_TODO_LI,
5 | isBlockAboveEmpty,
6 | isSelectionAtBlockStart,
7 | ResetNodePlugin,
8 | } from '@udecode/plate'
9 | import { PlatePlugin } from '@udecode/plate'
10 |
11 | const resetBlockTypesCommonRule = {
12 | types: [ELEMENT_BLOCKQUOTE, ELEMENT_TODO_LI],
13 | defaultType: ELEMENT_PARAGRAPH,
14 | }
15 |
16 | export const resetBlockTypePlugin: Partial> = {
17 | options: {
18 | rules: [
19 | {
20 | ...resetBlockTypesCommonRule,
21 | hotkey: 'Enter',
22 | predicate: isBlockAboveEmpty,
23 | },
24 | {
25 | ...resetBlockTypesCommonRule,
26 | hotkey: 'Backspace',
27 | predicate: isSelectionAtBlockStart,
28 | },
29 | ],
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Splash.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 |
5 | const hide = keyframes`
6 | 0% {
7 | opacity: 1;
8 | }
9 | 90% {
10 | opacity: 0;
11 | }
12 | 100% {
13 | opacity: 0;
14 | visibility: hidden;
15 | }
16 | `
17 |
18 | const Splash = styled.div`
19 | position: fixed;
20 | top: 0;
21 | left: 0;
22 | right: 0;
23 | bottom: 0;
24 | z-index: 9999;
25 | pointer-events: none;
26 | background-color: ${theme('color.primary.surface')};
27 | animation-name: ${hide};
28 | animation-duration: ${theme('animation.time.long')};
29 | animation-timing-function: cubic-bezier(0.17, 0.18, 0.41, 0.99);
30 | animation-fill-mode: forwards;
31 | animation-delay: ${theme('animation.time.long')};
32 | `
33 |
34 | export { Splash }
35 |
--------------------------------------------------------------------------------
/src/utils/dates.ts:
--------------------------------------------------------------------------------
1 | function createDays(year: number, month: number) {
2 | let mon = month - 1 // months in JS are 0..11, not 1..12
3 | let d = new Date(year, mon)
4 |
5 | let days = []
6 |
7 | while (d.getMonth() == mon) {
8 | // days.push(`${year}${month}${d.getDate()}`)
9 | days.push(d.getDate())
10 | d.setDate(d.getDate() + 1)
11 | }
12 |
13 | return days
14 | }
15 |
16 | function getYearsSince(year: number) {
17 | let years = []
18 | let currentYear = new Date().getFullYear()
19 | while (year <= currentYear) {
20 | years.push(year)
21 | year++
22 | }
23 | return years
24 | }
25 |
26 | const stripeEpochToDate = (secs: number) => {
27 | var t = new Date('1970-01-01T00:30:00Z') // Unix epoch start.
28 | t.setSeconds(secs)
29 | return t
30 | }
31 |
32 | export { createDays, getYearsSince, stripeEpochToDate }
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "module": "es6",
5 | "skipLibCheck": true,
6 | "esModuleInterop": true,
7 | "noImplicitAny": true,
8 | "jsx": "react-jsx",
9 | "sourceMap": true,
10 | "baseUrl": ".",
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "paths": {
15 | "*": ["*"],
16 | "components": ["./src/components/"],
17 | "hooks": ["./src/hooks/"],
18 | "utils": ["./src/utils/"],
19 | "themes": ["./src/themes/"],
20 | "types": ["./src/types/"],
21 | "consts": ["./src/constants/"],
22 | "config": ["./src/config/"],
23 | "context": ["./src/context/"]
24 | },
25 | "lib": ["es2021", "dom"],
26 | "target": "es2021"
27 | },
28 | "include": ["src/**/*", "decs.d.ts"]
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Billing/Balance.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
2 | import { HeaderStyled, TextStyled } from './styled'
3 | import { displayAmount } from 'utils'
4 | import type { PaymentMethodProps } from './types'
5 | import dayjs from 'dayjs'
6 | import { theme } from 'themes'
7 | import relativeTime from 'dayjs/plugin/relativeTime'
8 | dayjs.extend(relativeTime)
9 |
10 | const Balance = ({ billingInfo, isLoading }: PaymentMethodProps) => {
11 | return (
12 |
13 |
14 | {isLoading ? : displayAmount(-billingInfo.customer.balance)}
15 |
16 | Balance
17 |
18 | )
19 | }
20 |
21 | export { Balance }
22 |
--------------------------------------------------------------------------------
/src/components/Icon/BlockBulletList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function BlockBulletList({ tintColor, ...props }: any) {
5 | return (
6 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Upgrade/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { SectionTitleStyled } from '../styled'
3 | import { Features } from './Features'
4 | import { Products } from './Products'
5 | import { useUserContext } from 'context'
6 |
7 | const UpgradeTabContent = () => {
8 | const { session, subscription } = useUserContext()
9 |
10 | useEffect(() => {
11 | window.electronAPI.capture({
12 | distinctId: session.user.id,
13 | event: 'settings view-tab',
14 | properties: { tab: 'upgrade' },
15 | })
16 | }, [])
17 |
18 | return (
19 | <>
20 |
21 | {subscription == null ? 'Upgrade your plan' : 'Plans'}
22 |
23 |
24 |
25 | >
26 | )
27 | }
28 |
29 | export { UpgradeTabContent }
30 |
--------------------------------------------------------------------------------
/src/services/saveFile.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain, dialog } from 'electron'
2 | import { logger, isDev } from '../utils'
3 | import fs from 'fs'
4 |
5 | ipcMain.handle('journal-export', async (event, data: string, format: 'txt' | 'json') => {
6 | logger('journal-export')
7 | const options = {
8 | title: 'Save file',
9 | defaultPath: 'Journal',
10 | buttonLabel: 'Save',
11 |
12 | filters: [
13 | { name: format, extensions: [format] },
14 | { name: 'All Files', extensions: ['*'] },
15 | ],
16 | }
17 |
18 | dialog.showSaveDialog(null, options).then(({ filePath }) => {
19 | if (filePath) {
20 | try {
21 | fs.writeFileSync(filePath, data, 'utf8')
22 | } catch (error) {
23 | dialog.showMessageBox({
24 | message: 'Failed to save the file!',
25 | type: 'error',
26 | })
27 | }
28 | }
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/components/Entry/LimitReached.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { useUserContext } from 'context'
3 | import { UpgradeButtonStyled, LimitReachedTextStyled, LimitReachedWrapperStyled } from './styled'
4 |
5 | const LimitReached = () => {
6 | const { invokeOpenSettings } = useUserContext()
7 | const { session } = useUserContext()
8 |
9 | const onClickHandler = () => {
10 | invokeOpenSettings.current(true)
11 | window.electronAPI.capture({
12 | distinctId: session.user.id,
13 | event: 'entry upgrade-cta',
14 | })
15 | }
16 |
17 | return (
18 |
19 | Free plan limit reached...
20 | Upgrade
21 |
22 | )
23 | }
24 |
25 | export { LimitReached }
26 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/ChangeCycle/LeftPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import {
5 | getCustomer,
6 | fetchCountries,
7 | calcYearlyPlanSavings,
8 | } from '../../../../context/UserContext/subscriptions'
9 |
10 | const LeftPanelStyled = styled.div`
11 | width: 260px;
12 | color: ${theme('color.primary.main')};
13 | font-weight: 500;
14 | font-size: 21px;
15 | line-height: 26px;
16 | & em {
17 | opacity: 0.6;
18 | font-style: normal;
19 | }
20 | `
21 | interface LeftPanelProps {
22 | saving: string
23 | }
24 |
25 | const LeftPanel = ({ saving }: LeftPanelProps) => {
26 | return (
27 |
28 | Give yourself
29 |
30 | whole year
31 |
32 | of writing,
33 |
34 | {`save ${saving}`}
35 |
36 | )
37 | }
38 |
39 | export { LeftPanel }
40 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatOperation.ts:
--------------------------------------------------------------------------------
1 | import { AutoformatRule, ELEMENT_DEFAULT } from '@udecode/plate'
2 | import { DIGITS, DIGITS_WITH_SPACE } from './autoformatConstants'
3 | import { formatText } from './autoformatUtils'
4 |
5 | const multiplicationWithoutSpace: AutoformatRule[] = DIGITS.map((digit) => ({
6 | type: ELEMENT_DEFAULT,
7 | mode: 'block',
8 | match: [`${digit}*`, `${digit}x`],
9 | trigger: [...DIGITS, ...DIGITS_WITH_SPACE],
10 | insertTrigger: true,
11 | format: (editor) => formatText(editor, `${digit}×`),
12 | }))
13 |
14 | const multiplicationWithSpace: AutoformatRule[] = DIGITS.map((digit) => ({
15 | type: ELEMENT_DEFAULT,
16 | mode: 'block',
17 | match: [`${digit} *`, `${digit} x`],
18 | trigger: [...DIGITS, ...DIGITS_WITH_SPACE],
19 | insertTrigger: true,
20 | format: (editor) => formatText(editor, `${digit} ×`),
21 | }))
22 |
23 | export const autoformatMultiplication: AutoformatRule[] = [
24 | ...multiplicationWithoutSpace,
25 | ...multiplicationWithSpace,
26 | ]
27 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatBold.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatBold({ tintColor, ...props }: any) {
5 | return (
6 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/sql/schema.0.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- Create a table for Users
2 | create table if not exists users (
3 | id text,
4 | full_name text,
5 |
6 | PRIMARY KEY (id)
7 | );
8 |
9 | -- Create a table for Journals
10 | create table if not exists journals (
11 | user_id text,
12 | day date,
13 | created_at datetime,
14 | modified_at datetime,
15 | content text,
16 | deleted boolean not null default false,
17 | needs_saving_to_server boolean not null default false,
18 |
19 | PRIMARY KEY (user_id, day),
20 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE NO ACTION
21 | );
22 |
23 | -- Create a table for user Preferences
24 | create table if not exists preferences (
25 | user_id text,
26 | item text,
27 | value text,
28 |
29 | PRIMARY KEY (user_id, item),
30 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE NO ACTION
31 | );
32 |
33 | -- Create a table for app
34 | create table if not exists app (
35 | key text not null,
36 | value text,
37 |
38 | PRIMARY KEY (key)
39 | );
--------------------------------------------------------------------------------
/webpack.rules.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | // Add support for native node modules
3 | {
4 | // We're specifying native_modules in the test because the asset relocator loader generates a
5 | // "fake" .node file which is really a cjs file.
6 | test: /native_modules\/.+\.node$/,
7 | use: 'node-loader',
8 | },
9 | {
10 | test: /\.(m?js|node)$/,
11 | parser: { amd: false },
12 | use: {
13 | loader: '@vercel/webpack-asset-relocator-loader',
14 | options: {
15 | outputAssetBase: 'native_modules',
16 | },
17 | },
18 | },
19 | {
20 | test: /\.tsx?$/,
21 | exclude: /(node_modules|\.webpack)/,
22 | use: {
23 | loader: 'ts-loader',
24 | options: {
25 | transpileOnly: true,
26 | },
27 | },
28 | },
29 | {
30 | test: /\.png/,
31 | type: 'asset/resource',
32 | },
33 | {
34 | test: /\.sql/,
35 | type: 'asset/source',
36 | },
37 | {
38 | test: /\.m?js/,
39 | resolve: {
40 | fullySpecified: false,
41 | },
42 | },
43 | ]
44 |
--------------------------------------------------------------------------------
/src/sql/schema.1.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- Create a table for Users
2 | create table if not exists users (
3 | id text,
4 | full_name text,
5 | secret_key blob,
6 |
7 | PRIMARY KEY (id)
8 | );
9 |
10 | -- Create a table for Journals
11 | create table if not exists journals (
12 | user_id text,
13 | day date,
14 | created_at datetime,
15 | modified_at datetime,
16 | content text,
17 | deleted boolean not null default false,
18 | needs_saving_to_server boolean not null default false,
19 |
20 | PRIMARY KEY (user_id, day),
21 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE NO ACTION
22 | );
23 |
24 | -- Create a table for user Preferences
25 | create table if not exists preferences (
26 | user_id text,
27 | item text,
28 | value text,
29 |
30 | PRIMARY KEY (user_id, item),
31 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE NO ACTION
32 | );
33 |
34 | -- Create a table for app
35 | create table if not exists app (
36 | key text not null,
37 | value text,
38 |
39 | PRIMARY KEY (key)
40 | );
--------------------------------------------------------------------------------
/src/services/autoUpdater.ts:
--------------------------------------------------------------------------------
1 | import { isDev, logger } from '../utils'
2 | import { autoUpdater, ipcMain } from 'electron'
3 | import log from 'electron-log'
4 |
5 | if (!isDev()) {
6 | const server = 'https://desktop.journal.do'
7 | const updateUrl = `${server}/${process.platform}/${process.arch}/update.json`
8 | logger(`updateUrl: ${updateUrl}`)
9 | log.info(`updateUrl: ${updateUrl}`)
10 | autoUpdater.setFeedURL({ url: updateUrl, serverType: 'json' })
11 |
12 | autoUpdater.on('update-available', () => {
13 | logger('update-available')
14 | log.info('update-available')
15 | })
16 |
17 | autoUpdater.on('update-not-available', () => {
18 | logger('update-not-available')
19 | log.info('update-not-available')
20 | })
21 |
22 | setInterval(() => {
23 | logger('autoUpdater.checkForUpdates()')
24 | log.info('autoUpdater.checkForUpdates()')
25 | autoUpdater.checkForUpdates()
26 | }, 5 * 60 * 1000)
27 |
28 | autoUpdater.checkForUpdates()
29 |
30 | ipcMain.on('electron-quit-and-install', () => {
31 | autoUpdater.quitAndInstall()
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/tests/helpers/sqlite.ts:
--------------------------------------------------------------------------------
1 | import sqlite3 from 'sqlite3'
2 | import { open } from 'sqlite'
3 | import * as fs from 'fs'
4 |
5 | const dbFile = 'cache-test.db'
6 |
7 | export async function sqliteGetDB(userDataPath: string, user_id: string, day: string) {
8 | const db = await open({
9 | filename: userDataPath + '/' + dbFile,
10 | driver: sqlite3.Database,
11 | })
12 | const result = await db.get(
13 | `select * from journals where user_id = '${user_id}' and day = '${day}'`
14 | )
15 | return result
16 | }
17 |
18 | export async function sqliteDeleteDB(userDataPath: string) {
19 | await fs.promises.unlink(userDataPath + '/' + dbFile)
20 | }
21 |
22 | export async function sqliteRenameDB(userDataPath: string, name: string) {
23 | await fs.promises.rename(
24 | userDataPath + '/' + dbFile,
25 | userDataPath + '/' + `cache-test-${name}.db`
26 | )
27 | }
28 |
29 | export async function sqliteCreateCopyDB(userDataPath: string, name: string) {
30 | await fs.promises.copyFile(
31 | userDataPath + '/' + dbFile,
32 | userDataPath + '/' + `cache-test-${name}.db`
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/FadeOut.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { theme } from 'themes'
4 |
5 | const FadeTop = styled.div`
6 | position: fixed;
7 | pointer-events: none;
8 | top: 0;
9 | left: 0;
10 | right: 0;
11 | height: 56px;
12 | z-index: 10;
13 | background: linear-gradient(
14 | 180deg,
15 | ${theme('color.primary.surface')} 0%,
16 | ${theme('color.primary.surface')} 30%,
17 | ${theme('color.primary.surface', 0)} 100%
18 | );
19 | `
20 | // background: linear-gradient(180deg, #e0e0e0 0%, #e0e0e0 35%, rgba(224, 224, 224, 0) 100%);
21 |
22 | const FadeDown = styled.div`
23 | position: fixed;
24 | pointer-events: none;
25 | bottom: 0;
26 | left: 0;
27 | right: 0;
28 | height: 56px;
29 | z-index: 10;
30 | background: linear-gradient(
31 | 0deg,
32 | ${theme('color.primary.surface')} 0%,
33 | ${theme('color.primary.surface')} 30%,
34 | ${theme('color.primary.surface', 0)} 100%
35 | );
36 | `
37 |
38 | function FadeOut() {
39 | return (
40 | <>
41 |
42 |
43 | >
44 | )
45 | }
46 |
47 | export { FadeOut }
48 |
--------------------------------------------------------------------------------
/src/components/Icon/BlockH2.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function BlockH2({ tintColor, ...props }: any) {
5 | return (
6 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/TrafficLightMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled from 'styled-components'
3 | import { theme } from 'themes'
4 | import { useAppearanceContext, AppearanceContextInterface } from 'context'
5 | import { Icon } from 'components'
6 |
7 | const Container = styled.div`
8 | position: fixed;
9 | display: flex;
10 | gap: 8px;
11 | top: 16px;
12 | left: 15px;
13 | z-index: 9999;
14 | opacity: 0.3;
15 | transition: opacity ${theme('animation.time.normal')};
16 | &:hover {
17 | opacity: 0.7;
18 | }
19 | `
20 |
21 | const ToggleButton = styled.button`
22 | border: 0;
23 | outline: 0;
24 | padding: 0;
25 | background-color: transparent;
26 | `
27 |
28 | const TrafficLightMenu = () => {
29 | const { isCalendarOpen, toggleIsCalendarOpen } = useAppearanceContext()
30 | return (
31 |
32 |
33 | toggleIsCalendarOpen()}>
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export { TrafficLightMenu }
41 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatUtils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AutoformatBlockRule,
3 | ELEMENT_CODE_BLOCK,
4 | ELEMENT_CODE_LINE,
5 | getParentNode,
6 | isElement,
7 | isType,
8 | PlateEditor,
9 | TEditor,
10 | toggleList,
11 | unwrapList,
12 | } from '@udecode/plate'
13 |
14 | export const clearBlockFormat: AutoformatBlockRule['preFormat'] = (editor) =>
15 | unwrapList(editor as PlateEditor)
16 |
17 | export const format = (editor: TEditor, customFormatting: any) => {
18 | if (editor.selection) {
19 | const parentEntry = getParentNode(editor, editor.selection)
20 | if (!parentEntry) return
21 | const [node] = parentEntry
22 | if (
23 | isElement(node) &&
24 | !isType(editor as PlateEditor, node, ELEMENT_CODE_BLOCK) &&
25 | !isType(editor as PlateEditor, node, ELEMENT_CODE_LINE)
26 | ) {
27 | customFormatting()
28 | }
29 | }
30 | }
31 |
32 | export const formatList = (editor: TEditor, elementType: string) => {
33 | format(editor, () =>
34 | toggleList(editor as PlateEditor, {
35 | type: elementType,
36 | })
37 | )
38 | }
39 |
40 | export const formatText = (editor: TEditor, text: string) => {
41 | format(editor, () => editor.insertText(text))
42 | }
43 |
--------------------------------------------------------------------------------
/src/types/tags.ts:
--------------------------------------------------------------------------------
1 | import { lightTheme, theme } from 'themes'
2 |
3 | type SyncStatus = 'synced' | 'pending_insert' | 'pending_update' | 'pending_delete'
4 |
5 | type Tag = {
6 | id: string
7 | user_id?: string
8 | name: string
9 | color: keyof typeof lightTheme.color.tags
10 | created_at?: string
11 | modified_at?: string
12 | revision?: number
13 | sync_status?: SyncStatus
14 | }
15 |
16 | type EntryTag = {
17 | user_id?: string
18 | day: string
19 | journal_id?: number
20 | tag_id: string
21 | order_no: number
22 | created_at?: string
23 | modified_at?: string
24 | revision?: number
25 | sync_status?: SyncStatus
26 | }
27 |
28 | type EntryTagProperty =
29 | | { user_id: string }
30 | | { day: string }
31 | | { journal_id: number }
32 | | { tag_id: string }
33 | | { order_no: number }
34 | | { created_at: string }
35 | | { modified_at: string }
36 | | { revision: number }
37 | | { sync_status: SyncStatus }
38 |
39 | type ListItemActionType = {
40 | type: 'action'
41 | value: 'CREATE'
42 | }
43 |
44 | type ListItemTagType = {
45 | type: 'tag'
46 | value: Tag
47 | }
48 |
49 | type ListItemType = ListItemActionType | ListItemTagType
50 |
51 | export { Tag, EntryTag, EntryTagProperty, ListItemType }
52 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatLists.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AutoformatRule,
3 | ELEMENT_LI,
4 | ELEMENT_OL,
5 | ELEMENT_TODO_LI,
6 | ELEMENT_UL,
7 | setNodes,
8 | TElement,
9 | TTodoListItemElement,
10 | } from '@udecode/plate'
11 | import { clearBlockFormat, formatList } from './autoformatUtils'
12 |
13 | export const autoformatLists: AutoformatRule[] = [
14 | {
15 | mode: 'block',
16 | type: ELEMENT_LI,
17 | match: ['* ', '- '],
18 | preFormat: clearBlockFormat,
19 | format: (editor) => formatList(editor, ELEMENT_UL),
20 | },
21 | {
22 | mode: 'block',
23 | type: ELEMENT_LI,
24 | match: ['1. ', '1) '],
25 | preFormat: clearBlockFormat,
26 | format: (editor) => formatList(editor, ELEMENT_OL),
27 | },
28 | // {
29 | // mode: 'block',
30 | // type: ELEMENT_TODO_LI,
31 | // match: '[] ',
32 | // },
33 | // {
34 | // mode: 'block',
35 | // type: ELEMENT_TODO_LI,
36 | // match: '[x] ',
37 | // format: (editor) =>
38 | // setNodes>(
39 | // editor,
40 | // { type: ELEMENT_TODO_LI, checked: true },
41 | // {
42 | // match: (n) => Editor.isBlock(editor, n),
43 | // }
44 | // ),
45 | // },
46 | ]
47 |
--------------------------------------------------------------------------------
/src/utils/supabaseClient.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@supabase/supabase-js'
2 | import { isDev } from './'
3 |
4 | const supabaseEnv = {
5 | local: {
6 | supabaseUrl: 'https://supabase.journal.local:8443',
7 | supabaseAnonKey:
8 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRuZmRhdW9vd3lycHhxb2RvbXFuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTQ1MzEzOTUsImV4cCI6MTk3MDEwNzM5NX0.XYkcWry-Eqm0-Hvq-arndEGhQn_yJvGF85-NNf9Sbvk',
9 | },
10 | stg: {
11 | supabaseUrl: 'https://tnfdauoowyrpxqodomqn.supabase.co',
12 | supabaseAnonKey:
13 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRuZmRhdW9vd3lycHhxb2RvbXFuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTQ1MzEzOTUsImV4cCI6MTk3MDEwNzM5NX0.XYkcWry-Eqm0-Hvq-arndEGhQn_yJvGF85-NNf9Sbvk',
14 | },
15 | prod: {
16 | supabaseUrl: 'https://hsbagpjhlxzabpiitqjw.supabase.co',
17 | supabaseAnonKey:
18 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhzYmFncGpobHh6YWJwaWl0cWp3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTI0MjMwOTEsImV4cCI6MTk2Nzk5OTA5MX0.O-QNy1tbJ7AvZMRhBf8i7_UDNUDhBMQ_yKJEEeS5p84',
19 | },
20 | }
21 |
22 | const { supabaseUrl } = supabaseEnv[isDev() ? 'local' : 'prod']
23 | const { supabaseAnonKey } = supabaseEnv[isDev() ? 'local' : 'prod']
24 |
25 | const supabase = createClient(supabaseUrl, supabaseAnonKey)
26 |
27 | export { supabaseUrl, supabaseAnonKey, supabase }
28 |
--------------------------------------------------------------------------------
/src/components/Icon/BlockText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function BlockText({ tintColor, ...props }: any) {
5 | return (
6 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Icon/TrafficLightCalendar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function TrafficLightCalendar({ tintColor, type = 'down', ...props }: any) {
5 | switch (type) {
6 | case 'off':
7 | return (
8 |
22 | )
23 | case 'on':
24 | return (
25 |
41 | )
42 | default:
43 | return <>>
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatStriketrough.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatStriketrough({ tintColor, ...props }: any) {
5 | return (
6 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Subscribe/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import * as Tabs from '@radix-ui/react-tabs'
3 | import styled, { keyframes } from 'styled-components'
4 | import { theme } from 'themes'
5 | import { Icon } from 'components'
6 | import { CardElement } from '@stripe/react-stripe-js'
7 |
8 | const TextStyled = styled.div`
9 | font-weight: 500;
10 | font-size: 14px;
11 | line-height: 24px;
12 | `
13 |
14 | const CheckoutModalStyled = styled.div`
15 | background-color: ${theme('color.popper.surface')};
16 | display: flex;
17 | position: relative;
18 | padding: 0;
19 | padding: 40px 32px 32px 32px;
20 | margin: 48px 8px 8px 8px;
21 | border-radius: 8px;
22 | -webkit-app-region: no-drag;
23 | `
24 |
25 | const ButtonStyled = styled.button`
26 | background-color: ${theme('color.popper.main')};
27 | color: ${theme('color.popper.inverted')};
28 | outline: 0;
29 | border: 0;
30 | font-weight: 500;
31 | font-size: 14px;
32 | line-height: 24px;
33 | cursor: pointer;
34 | display: flex;
35 | margin-top: 16px;
36 | padding: 8px 12px;
37 | border-radius: 6px;
38 | width: fit-content;
39 | transition: box-shadow ${theme('animation.time.normal')} ease;
40 | &:hover,
41 | &:focus {
42 | box-shadow: 0 0 0 2px ${theme('color.popper.main', 0.15)};
43 | }
44 | &:disabled {
45 | opacity: 0.6;
46 | cursor: default;
47 | }
48 | `
49 |
50 | export { TextStyled, CheckoutModalStyled, ButtonStyled }
51 |
--------------------------------------------------------------------------------
/src/components/Icon/Cross.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Cross({ tintColor, size, ...props }: any) {
5 | switch (size) {
6 | case 12:
7 | return (
8 |
16 | )
17 |
18 | case 16:
19 | return (
20 |
28 | )
29 |
30 | default:
31 | return (
32 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/ImportExport/nodeTypes.ts:
--------------------------------------------------------------------------------
1 | import { InputNodeTypes } from 'remark-slate'
2 | import {
3 | ELEMENT_BLOCKQUOTE,
4 | ELEMENT_CODE_BLOCK,
5 | ELEMENT_CODE_LINE,
6 | ELEMENT_H1,
7 | ELEMENT_H2,
8 | ELEMENT_H3,
9 | ELEMENT_H4,
10 | ELEMENT_H5,
11 | ELEMENT_H6,
12 | ELEMENT_IMAGE,
13 | ELEMENT_LI,
14 | ELEMENT_LIC,
15 | ELEMENT_LINK,
16 | ELEMENT_MEDIA_EMBED,
17 | ELEMENT_MENTION,
18 | ELEMENT_MENTION_INPUT,
19 | ELEMENT_OL,
20 | ELEMENT_PARAGRAPH,
21 | ELEMENT_TABLE,
22 | ELEMENT_TD,
23 | ELEMENT_TH,
24 | ELEMENT_TODO_LI,
25 | ELEMENT_TR,
26 | ELEMENT_UL,
27 | MARK_BOLD,
28 | MARK_CODE,
29 | MARK_ITALIC,
30 | MARK_STRIKETHROUGH,
31 | } from '@udecode/plate'
32 | // Override the default remark-slate node type names to match Plate defaults
33 | //format: :;
34 |
35 | const plateNodeTypes: InputNodeTypes = {
36 | paragraph: ELEMENT_PARAGRAPH,
37 | block_quote: ELEMENT_BLOCKQUOTE,
38 | code_block: ELEMENT_CODE_BLOCK,
39 | link: ELEMENT_LINK,
40 | ul_list: ELEMENT_UL,
41 | ol_list: ELEMENT_OL,
42 | listItem: ELEMENT_LI,
43 | heading: {
44 | 1: ELEMENT_H1,
45 | 2: ELEMENT_H2,
46 | 3: ELEMENT_H3,
47 | 4: ELEMENT_H4,
48 | 5: ELEMENT_H5,
49 | 6: ELEMENT_H6,
50 | },
51 | emphasis_mark: MARK_ITALIC,
52 | strong_mark: MARK_BOLD,
53 | delete_mark: MARK_STRIKETHROUGH,
54 | inline_code_mark: MARK_CODE,
55 | thematic_break: 'thematic_break',
56 | image: ELEMENT_IMAGE,
57 | }
58 |
59 | export { plateNodeTypes }
60 |
--------------------------------------------------------------------------------
/src/components/Icon/BlockH3.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function BlockH3({ tintColor, ...props }: any) {
5 | return (
6 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Icon/BlockNumList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function BlockNumList({ tintColor, ...props }: any) {
5 | return (
6 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/crypto.ts:
--------------------------------------------------------------------------------
1 | // declare var TextDecoder: any
2 | var ab2str = require('arraybuffer-to-string')
3 | var str2ab = require('string-to-arraybuffer')
4 |
5 | const fromHex = (h: string) => {
6 | if (h.slice(0, 2) == '\\x') {
7 | h = h.slice(2)
8 | }
9 | var s = ''
10 | for (var i = 0; i < h.length; i += 2) {
11 | s += String.fromCharCode(parseInt(h.slice(i, i + 2), 16))
12 | }
13 | return s
14 | }
15 |
16 | const encryptEntry = async (content: any, secretKey: CryptoKey) => {
17 | let utf8Encoder = new TextEncoder()
18 |
19 | let ivBuffer = window.crypto.getRandomValues(new Uint8Array(16))
20 | let contentBuffer = await window.crypto.subtle.encrypt(
21 | {
22 | name: 'AES-CTR',
23 | counter: ivBuffer,
24 | length: 32,
25 | },
26 | secretKey,
27 | utf8Encoder.encode(content)
28 | )
29 | let contentEncrypted = ab2str(contentBuffer, 'hex')
30 | let iv = ab2str(ivBuffer, 'hex')
31 |
32 | return { contentEncrypted, iv }
33 | }
34 |
35 | const decryptEntry = async (content: any, iv: string, secretKey: CryptoKey) => {
36 | let ivUtf = fromHex(iv)
37 | let ivAb = str2ab(ivUtf)
38 |
39 | let contentUtf = fromHex(content)
40 | let contentAb = str2ab(contentUtf)
41 |
42 | let contentBuffer = await window.crypto.subtle.decrypt(
43 | {
44 | name: 'AES-CTR',
45 | counter: ivAb,
46 | length: 32,
47 | },
48 | secretKey,
49 | contentAb
50 | )
51 | let contentDecrypted = ab2str(contentBuffer, 'utf8') as string
52 |
53 | return { contentDecrypted }
54 | }
55 |
56 | export { encryptEntry, decryptEntry }
57 |
--------------------------------------------------------------------------------
/src/components/Icon/Exit.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | // export function Exit({ tintColor, ...props }: any) {
5 | // return (
6 | //
14 | // )
15 | // }
16 |
17 | export function Exit({ tintColor, ...props }: any) {
18 | return (
19 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/themes/index.ts:
--------------------------------------------------------------------------------
1 | import { lightTheme } from './lightTheme'
2 | import { darkTheme } from './darkTheme'
3 | import { forestTheme } from './forestTheme'
4 | import { cappuccinoTheme } from './cappuccinoTheme'
5 | import { baseTheme } from 'config'
6 |
7 | type RecursiveKeyOf = {
8 | [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue
9 | }[keyof TObj & (string | number)]
10 |
11 | type RecursiveKeyOfInner = {
12 | [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue
13 | }[keyof TObj & (string | number)]
14 |
15 | type RecursiveKeyOfHandleValue = TValue extends any[]
16 | ? Text
17 | : TValue extends object
18 | ? Text | `${Text}${RecursiveKeyOfInner}`
19 | : Text
20 |
21 | type LightThemeItemKey = RecursiveKeyOf
22 | type BaseThemeItemKey = RecursiveKeyOf
23 |
24 | const theme = (itemKey: LightThemeItemKey | BaseThemeItemKey, alpha = 1) => {
25 | const cssVar = itemKey.split('.').reduce((acc, key) => acc + '-' + key, '-')
26 | if (itemKey.split('.')[0] == 'color') {
27 | return `rgba(var(${cssVar}), ${alpha})`
28 | }
29 | return `var(${cssVar})`
30 | }
31 |
32 | const getCSSVar = (itemKey: LightThemeItemKey | BaseThemeItemKey) => {
33 | return itemKey.split('.').reduce((acc, key) => acc + '-' + key, '-')
34 | }
35 |
36 | export {
37 | theme,
38 | getCSSVar,
39 | lightTheme,
40 | darkTheme,
41 | baseTheme,
42 | forestTheme,
43 | cappuccinoTheme,
44 | LightThemeItemKey,
45 | BaseThemeItemKey,
46 | }
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 | .DS_Store
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # TypeScript cache
43 | *.tsbuildinfo
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 | .env.test
63 |
64 | # parcel-bundler cache (https://parceljs.org/)
65 | .cache
66 |
67 | # next.js build output
68 | .next
69 |
70 | # nuxt.js build output
71 | .nuxt
72 |
73 | # vuepress build output
74 | .vuepress/dist
75 |
76 | # Serverless directories
77 | .serverless/
78 |
79 | # FuseBox cache
80 | .fusebox/
81 |
82 | # DynamoDB Local files
83 | .dynamodb/
84 |
85 | # Webpack
86 | .webpack/
87 |
88 | # Electron-Forge
89 | out/
90 |
91 | REPL
92 |
93 | test-output
--------------------------------------------------------------------------------
/src/components/Icon/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Settings({ tintColor, ...props }: any) {
5 | return (
6 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/FormatToolbar/BlockTypeSelectItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Icon } from 'components'
3 | import { theme } from 'themes'
4 | import styled from 'styled-components'
5 |
6 | const ItemWrapper = styled.button`
7 | display: flex;
8 | border: 0;
9 | gap: 8px;
10 | padding: 2px 12px 2px 4px;
11 | border-radius: 8px;
12 | cursor: pointer;
13 | width: 100%;
14 | background-color: ${theme('color.popper.surface')};
15 | align-items: center;
16 | transition: ${theme('animation.time.normal')};
17 | &:hover {
18 | background-color: ${theme('color.popper.hover')};
19 | }
20 | `
21 | type ItemCurrentProps = {
22 | current: boolean
23 | }
24 |
25 | const ItemTitle = styled.span`
26 | font-size: 14px;
27 | color: ${theme('color.popper.main')};
28 | font-weight: ${(props) => (props.current ? '700' : 'normal')};
29 | line-height: 20px;
30 | flex-grow: 1;
31 | text-align: left;
32 | `
33 |
34 | const ItemCurrent = styled(({ current, ...props }) => (
35 |
36 | ))`
37 | visibility: ${(props) => (props.current ? 'visible' : 'hidden')};
38 | `
39 |
40 | type ItemProps = {
41 | onMouseDown: (e: any) => void
42 | icon: string
43 | current: boolean
44 | children: string
45 | }
46 |
47 | const BlockTypeSelectItem = ({ onMouseDown, icon, current = false, children }: ItemProps) => {
48 | return (
49 |
50 |
51 | {children}
52 |
53 |
54 | )
55 | }
56 |
57 | export { BlockTypeSelectItem }
58 |
--------------------------------------------------------------------------------
/src/components/Icon/FormatHandStriketrough.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function FormatHandStriketrough({ tintColor, ...props }: any) {
5 | return (
6 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatMarks.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AutoformatRule,
3 | MARK_BOLD,
4 | MARK_CODE,
5 | MARK_HIGHLIGHT,
6 | MARK_ITALIC,
7 | MARK_STRIKETHROUGH,
8 | MARK_SUBSCRIPT,
9 | MARK_SUPERSCRIPT,
10 | MARK_UNDERLINE,
11 | } from '@udecode/plate'
12 |
13 | export const autoformatMarks: AutoformatRule[] = [
14 | {
15 | mode: 'mark',
16 | type: [MARK_BOLD, MARK_ITALIC],
17 | match: '***',
18 | },
19 | {
20 | mode: 'mark',
21 | type: [MARK_UNDERLINE, MARK_ITALIC],
22 | match: '__*',
23 | },
24 | {
25 | mode: 'mark',
26 | type: [MARK_UNDERLINE, MARK_BOLD],
27 | match: '__**',
28 | },
29 | {
30 | mode: 'mark',
31 | type: [MARK_UNDERLINE, MARK_BOLD, MARK_ITALIC],
32 | match: '___***',
33 | },
34 | {
35 | mode: 'mark',
36 | type: MARK_BOLD,
37 | match: '**',
38 | },
39 | {
40 | mode: 'mark',
41 | type: MARK_UNDERLINE,
42 | match: '__',
43 | },
44 | {
45 | mode: 'mark',
46 | type: MARK_ITALIC,
47 | match: '*',
48 | },
49 | {
50 | mode: 'mark',
51 | type: MARK_ITALIC,
52 | match: '_',
53 | },
54 | {
55 | mode: 'mark',
56 | type: MARK_STRIKETHROUGH,
57 | match: '~~',
58 | },
59 | // {
60 | // mode: 'mark',
61 | // type: MARK_SUPERSCRIPT,
62 | // match: '^',
63 | // },
64 | // {
65 | // mode: 'mark',
66 | // type: MARK_SUBSCRIPT,
67 | // match: '~',
68 | // },
69 | // {
70 | // mode: 'mark',
71 | // type: MARK_HIGHLIGHT,
72 | // match: '==',
73 | // },
74 | // {
75 | // mode: 'mark',
76 | // type: MARK_HIGHLIGHT,
77 | // match: '≡',
78 | // },
79 | {
80 | mode: 'mark',
81 | type: MARK_CODE,
82 | match: '`',
83 | },
84 | ]
85 |
--------------------------------------------------------------------------------
/src/components/Icon/Bucket.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | // export function Bucket({ tintColor, ...props }: any) {
5 | // return (
6 | //
20 | // )
21 | // }
22 |
23 | export function Bucket({ tintColor, ...props }: any) {
24 | return (
25 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Billing/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { logger, supabase, stripeEpochToDate, isDev } from 'utils'
3 | import { SectionTitleStyled } from '../styled'
4 | import { useQuery } from '@tanstack/react-query'
5 | import { useUserContext } from 'context'
6 | import { getCustomer } from '../../../../context/UserContext/subscriptions'
7 | import { PaymentMethod } from './PaymentMethod'
8 | import { Receipts } from './Receipts'
9 | import { Plan } from './Plan'
10 | import { Balance } from './Balance'
11 | import { Divider } from './styled'
12 |
13 | const BillingTabContent = () => {
14 | logger('BillingTabContent re-render')
15 | const { session, subscription } = useUserContext()
16 | const {
17 | isLoading,
18 | isError,
19 | data: billingInfo,
20 | } = useQuery({
21 | queryKey: ['billingInfo'],
22 | queryFn: async () => getCustomer(session.access_token),
23 | })
24 |
25 | useEffect(() => {
26 | window.electronAPI.capture({
27 | distinctId: session.user.id,
28 | event: 'settings view-tab',
29 | properties: { tab: 'billing' },
30 | })
31 | }, [])
32 |
33 | useEffect(() => {
34 | logger(billingInfo)
35 | }, [billingInfo])
36 |
37 | return (
38 | <>
39 | Billing
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | >
48 | )
49 | }
50 |
51 | export { BillingTabContent }
52 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Subscribe/Success.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { Icon } from 'components'
3 | import styled, { keyframes } from 'styled-components'
4 | import { theme } from 'themes'
5 |
6 | const WrapperStyled = styled.div`
7 | padding: 16px 48px;
8 | display: flex;
9 | align-items: center;
10 | gap: 24px;
11 | flex-direction: column;
12 | `
13 |
14 | const MessageStyled = styled.div`
15 | color: ${theme('color.popper.main')};
16 | text-align: center;
17 | line-height: 20px;
18 | font-size: 14px;
19 | `
20 |
21 | const ButtonStyled = styled.button`
22 | font-weight: 500;
23 | font-size: 14px;
24 | line-height: 22px;
25 | cursor: pointer;
26 | color: ${theme('color.popper.main')};
27 | background-color: transparent;
28 | display: flex;
29 | align-items: flex-end;
30 | gap: 4px;
31 | padding: 8px 12px;
32 | border-radius: 6px;
33 | width: fit-content;
34 | border: 1px solid ${theme('color.popper.main')};
35 | outline: 0;
36 | transition: box-shadow ${theme('animation.time.normal')} ease;
37 | opacity: 0.8;
38 | &:hover {
39 | box-shadow: 0 0 0 4px ${theme('color.popper.main', 0.15)};
40 | }
41 | &:focus {
42 | box-shadow: 0 0 0 4px ${theme('color.popper.main', 0.15)};
43 | }
44 | `
45 |
46 | const Success = () => {
47 | return (
48 |
49 |
50 |
51 | Upgrade successful,
52 |
53 | you are all set!
54 |
55 | {/* window.electronAPI.reloadWindow()}>
56 | Back to journaling
57 | */}
58 |
59 | )
60 | }
61 |
62 | export { Success }
63 |
--------------------------------------------------------------------------------
/src/components/Icon/Check.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Check({ tintColor, size = 24, ...props }: any) {
5 | switch (size) {
6 | case 48:
7 | return (
8 |
23 | )
24 | case 24:
25 | return (
26 |
34 | )
35 | case 16:
36 | return (
37 |
45 | )
46 | default:
47 | return <>>
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/ScrollToToday.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import dayjs from 'dayjs'
5 | import { useUserContext, useEntriesContext } from 'context'
6 | import { select, focusEditor } from '@udecode/plate'
7 | import { logger } from 'utils'
8 |
9 | const ScrollToTodayButton = styled.button`
10 | position: fixed;
11 | bottom: 8px;
12 | left: 32px;
13 | margin-bottom: -32px;
14 | margin-left: ${theme('appearance.entriesOffset')};
15 | transition-duration: ${theme('animation.time.normal')};
16 | transition-timing-function: ${theme('animation.timingFunction.dynamic')};
17 | animation-duration: ${theme('animation.time.normal')};
18 | font-size: 12px;
19 | background-color: transparent;
20 | border: 0;
21 | color: ${theme('color.primary.main')};
22 | z-index: 100;
23 | opacity: 0.3;
24 | outline: 0;
25 | cursor: pointer;
26 | &:hover {
27 | opacity: 0.7;
28 | }
29 | `
30 |
31 | function ScrollToToday() {
32 | const { session } = useUserContext()
33 | const { editorsRef } = useEntriesContext()
34 | const scrollToToday = () => {
35 | let today = dayjs().format('YYYY-MM-DD')
36 | let entry = document.getElementById(`${today}-entry`)
37 | if (entry) {
38 | logger('scrollToToday')
39 | entry.scrollIntoView()
40 | const editor = editorsRef.current[today]
41 | if (editor) {
42 | focusEditor(editor)
43 | select(editor, {
44 | path: [0, 0],
45 | offset: 0,
46 | })
47 | }
48 | }
49 | window.electronAPI.capture({
50 | distinctId: session.user.id,
51 | event: 'entry scroll-to-today',
52 | })
53 | }
54 |
55 | return (
56 | scrollToToday()}>
57 | ↓ Back to Today
58 |
59 | )
60 | }
61 |
62 | export { ScrollToToday }
63 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/ChangeCycle/Success.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { Icon } from 'components'
3 | import styled, { keyframes } from 'styled-components'
4 | import { theme } from 'themes'
5 | import { useQuery } from '@tanstack/react-query'
6 | import { getCustomer } from '../../../../context/UserContext/subscriptions'
7 | import { useUserContext } from 'context'
8 |
9 | const WrapperStyled = styled.div`
10 | padding: 16px 48px;
11 | display: flex;
12 | align-items: center;
13 | gap: 24px;
14 | flex-direction: column;
15 | `
16 |
17 | const MessageStyled = styled.div`
18 | color: ${theme('color.popper.main')};
19 | text-align: center;
20 | line-height: 20px;
21 | font-size: 14px;
22 | `
23 |
24 | const ButtonStyled = styled.button`
25 | font-weight: 500;
26 | font-size: 14px;
27 | line-height: 22px;
28 | cursor: pointer;
29 | color: ${theme('color.popper.main')};
30 | background-color: transparent;
31 | display: flex;
32 | align-items: flex-end;
33 | gap: 4px;
34 | padding: 8px 12px;
35 | border-radius: 6px;
36 | width: fit-content;
37 | border: 1px solid ${theme('color.popper.main')};
38 | outline: 0;
39 | transition: box-shadow ${theme('animation.time.normal')} ease;
40 | opacity: 0.8;
41 | &:hover {
42 | box-shadow: 0 0 0 4px ${theme('color.popper.main', 0.15)};
43 | }
44 | &:focus {
45 | box-shadow: 0 0 0 4px ${theme('color.popper.main', 0.15)};
46 | }
47 | `
48 |
49 | const Success = () => {
50 | const { session } = useUserContext()
51 | useQuery({
52 | queryKey: ['billingInfo'],
53 | queryFn: async () => getCustomer(session.access_token),
54 | })
55 |
56 | return (
57 |
58 |
59 |
60 | Success,
61 |
62 | you are all set!
63 |
64 |
65 | )
66 | }
67 |
68 | export { Success }
69 |
--------------------------------------------------------------------------------
/src/components/Icon/Chevron.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { theme } from 'themes'
3 |
4 | export function Chevron({ tintColor, size = 16, type = 'down', ...props }: any) {
5 | switch (type) {
6 | case 'down':
7 | switch (size) {
8 | case 16:
9 | return (
10 |
18 | )
19 | case 8:
20 | return (
21 |
29 | )
30 | }
31 |
32 | case 'up':
33 | switch (size) {
34 | case 16:
35 | return (
36 |
44 | )
45 | case 8:
46 | return (
47 |
55 | )
56 | }
57 | default:
58 | return <>>
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/services/analytics.ts:
--------------------------------------------------------------------------------
1 | import { isDev, logger } from '../utils'
2 | import { ipcMain, app } from 'electron'
3 | import fetch from 'node-fetch'
4 |
5 | if (isDev()) {
6 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
7 | app.commandLine.appendSwitch('ignore-certificate-errors')
8 | app.commandLine.appendSwitch('allow-insecure-localhost', 'true')
9 | }
10 |
11 | interface EventMessage {
12 | distinctId: string
13 | event: string
14 | type?: 'event' | 'system' | 'error'
15 | properties?: Record
16 | }
17 |
18 | const capture = async ({ distinctId, event, properties, type }: EventMessage) => {
19 | if (!isDev()) {
20 | logger(`capture ${event}${type ? ' of type ' + type : ''}`)
21 |
22 | let appInfo = {
23 | version: app.getVersion(),
24 | platform: process.platform,
25 | arch: process.arch,
26 | locale: app.getLocale(),
27 | }
28 |
29 | try {
30 | const body = JSON.stringify({
31 | user_id: distinctId,
32 | app: appInfo,
33 | event,
34 | properties,
35 | ...(type && { type }),
36 | })
37 | const url = isDev() ? 'https://capture.journal.local' : 'https://capture.journal.do'
38 | // const url = 'https://capture.journal.do'
39 | const headers = { 'Content-Type': 'application/json', 'x-api-key': 'o4dqm2yb' }
40 | await fetch(url, { method: 'post', body, headers })
41 | } catch (error) {
42 | logger(`error`)
43 | logger(error)
44 | }
45 | } else {
46 | logger(`capture ${event}${type ? ' of type ' + type : ''} (not capturing)`)
47 | if (properties) logger(properties)
48 | }
49 | }
50 |
51 | ipcMain.handle(
52 | 'analytics-capture',
53 | async (e, { distinctId, event, properties, type }: EventMessage) => {
54 | capture({ distinctId, event, properties, type })
55 | }
56 | )
57 |
58 | ipcMain.handle(
59 | 'analytics-capture-error',
60 | async (e, { distinctId, event, properties }: EventMessage) => {
61 | capture({ distinctId, event, properties })
62 | }
63 | )
64 |
65 | export { capture, EventMessage }
66 |
--------------------------------------------------------------------------------
/src/config/autoformat/autoformatBlocks.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AutoformatRule,
3 | ELEMENT_BLOCKQUOTE,
4 | ELEMENT_CODE_BLOCK,
5 | ELEMENT_DEFAULT,
6 | ELEMENT_H1,
7 | ELEMENT_H2,
8 | ELEMENT_H3,
9 | ELEMENT_H4,
10 | ELEMENT_H5,
11 | ELEMENT_H6,
12 | ELEMENT_HR,
13 | getPluginType,
14 | insertEmptyCodeBlock,
15 | insertNodes,
16 | PlateEditor,
17 | setNodes,
18 | } from '@udecode/plate'
19 | import { clearBlockFormat } from './autoformatUtils'
20 |
21 | export const autoformatBlocks: AutoformatRule[] = [
22 | {
23 | mode: 'block',
24 | type: ELEMENT_H1,
25 | match: '# ',
26 | preFormat: clearBlockFormat,
27 | },
28 | {
29 | mode: 'block',
30 | type: ELEMENT_H2,
31 | match: '## ',
32 | preFormat: clearBlockFormat,
33 | },
34 | {
35 | mode: 'block',
36 | type: ELEMENT_H3,
37 | match: '### ',
38 | preFormat: clearBlockFormat,
39 | },
40 | // {
41 | // mode: 'block',
42 | // type: ELEMENT_H4,
43 | // match: '#### ',
44 | // preFormat: clearBlockFormat,
45 | // },
46 | // {
47 | // mode: 'block',
48 | // type: ELEMENT_H5,
49 | // match: '##### ',
50 | // preFormat: clearBlockFormat,
51 | // },
52 | // {
53 | // mode: 'block',
54 | // type: ELEMENT_H6,
55 | // match: '###### ',
56 | // preFormat: clearBlockFormat,
57 | // },
58 | // {
59 | // mode: 'block',
60 | // type: ELEMENT_BLOCKQUOTE,
61 | // match: '> ',
62 | // preFormat: clearBlockFormat,
63 | // },
64 | {
65 | mode: 'block',
66 | type: ELEMENT_HR,
67 | match: ['---', '—-'],
68 | preFormat: clearBlockFormat,
69 | format: (editor) => {
70 | setNodes(editor, { type: ELEMENT_HR })
71 | insertNodes(editor, {
72 | type: ELEMENT_DEFAULT,
73 | children: [{ text: '' }],
74 | })
75 | },
76 | },
77 | // {
78 | // mode: 'block',
79 | // type: ELEMENT_CODE_BLOCK,
80 | // match: '```',
81 | // triggerAtBlockStart: false,
82 | // preFormat: clearBlockFormat,
83 | // format: (editor) => {
84 | // insertEmptyCodeBlock(editor as PlateEditor, {
85 | // defaultType: getPluginType(editor as PlateEditor, ELEMENT_DEFAULT),
86 | // insertNodesOptions: { select: true },
87 | // })
88 | // },
89 | // },
90 | ]
91 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Billing/Receipts.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
2 | import { logger, capitalize, stripeEpochToDate, displayAmount } from 'utils'
3 | import { theme } from 'themes'
4 | import {
5 | HeaderStyled,
6 | TextStyled,
7 | ActionsStyled,
8 | ActionStyled,
9 | ContentStyled,
10 | CardStyled,
11 | ReceiptsRowStyled,
12 | ReceiptsTableStyled,
13 | ReceiptsCellStyled,
14 | DownloadStyled,
15 | } from './styled'
16 | import type { PaymentMethodProps } from './types'
17 | import dayjs from 'dayjs'
18 | import relativeTime from 'dayjs/plugin/relativeTime'
19 | dayjs.extend(relativeTime)
20 |
21 | const Receipts = ({ billingInfo, isLoading }: PaymentMethodProps) => {
22 | const Items = () => {
23 | const { invoices } = billingInfo
24 | return (
25 | <>
26 | {invoices.filter((invoice) => invoice.status == 'paid').length == 0 ? (
27 | 'No receipts'
28 | ) : (
29 |
30 | {invoices
31 | .filter((invoice) => invoice.status == 'paid')
32 | .map((invoice) => {
33 | return (
34 |
35 |
36 | {dayjs(stripeEpochToDate(invoice.period_start)).format('MMM D YYYY') +
37 | ' - ' +
38 | dayjs(stripeEpochToDate(invoice.period_end)).format('MMM D YYYY')}
39 |
40 | {displayAmount(invoice.amount_paid)}
41 | {capitalize(invoice.status)}
42 |
43 | PDF↓
44 |
45 |
46 | )
47 | })}
48 |
49 | )}
50 | >
51 | )
52 | }
53 |
54 | return (
55 |
56 | Receipts
57 | {isLoading ? : }
58 |
59 | )
60 | }
61 |
62 | export { Receipts }
63 |
--------------------------------------------------------------------------------
/src/components/Entry/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { theme } from 'themes'
3 | import { breakpoints } from 'utils'
4 |
5 | const Container = styled.div`
6 | display: flex;
7 | padding: 32px 0 32px 40px;
8 | word-break: break-word;
9 | @media ${breakpoints.s} {
10 | padding: 16px 24px;
11 | }
12 | `
13 |
14 | const MainWrapper = styled.div`
15 | width: min-content;
16 | contain: layout;
17 | flex-grow: 1;
18 | padding: 0 80px 0 0;
19 | font-size: ${theme('appearance.fontSize')};
20 | font-family: ${theme('appearance.fontFace')};
21 | font-weight: 500;
22 | line-height: 30px;
23 | -webkit-app-region: no-drag;
24 | & > div:nth-child(2) > h1:first-child,
25 | & > div:nth-child(2) > h2:first-child,
26 | & > div:nth-child(2) > h3:first-child,
27 | & > div:nth-child(2) > div:first-child > h1:first-child,
28 | & > div:nth-child(2) > div:first-child > h2:first-child,
29 | & > div:nth-child(2) > div:first-child > h3:first-child {
30 | margin-block-start: 0;
31 | }
32 | & > * {
33 | max-width: 75ch;
34 | color: ${theme('color.primary.main')};
35 | }
36 | @media ${breakpoints.s} {
37 | padding: 0;
38 | }
39 | `
40 | const MiniDate = styled.div`
41 | padding: 0 0 8px 0;
42 | margin: 0;
43 | opacity: 0.3;
44 | visibility: ${theme('appearance.miniDatesVisibility')};
45 | color: ${theme('color.primary.main')};
46 | font-size: 12px;
47 | font-family: 'Inter var';
48 | line-height: 16px;
49 | `
50 |
51 | const LimitReachedWrapperStyled = styled.div`
52 | display: flex;
53 | flex-direction: column;
54 | gap: 8px;
55 | `
56 |
57 | const LimitReachedTextStyled = styled.div`
58 | opacity: 0.4;
59 | `
60 |
61 | const UpgradeButtonStyled = styled.button`
62 | font-weight: 500;
63 | font-size: 14px;
64 | line-height: 24px;
65 | padding: 2px 8px;
66 | color: ${theme('color.primary.main')};
67 | border: 1px solid ${theme('color.primary.main')};
68 | border-radius: 6px;
69 | width: fit-content;
70 | background: transparent;
71 | cursor: pointer;
72 | outline: 0;
73 | &:hover,
74 | &:focus {
75 | box-shadow: 0 0 0 3px ${theme('color.primary.main', 0.2)};
76 | transition: box-shadow ${theme('animation.time.normal')} ease;
77 | }
78 | &:disabled {
79 | opacity: 0.6;
80 | cursor: default;
81 | }
82 | `
83 |
84 | export {
85 | Container,
86 | MainWrapper,
87 | MiniDate,
88 | UpgradeButtonStyled,
89 | LimitReachedTextStyled,
90 | LimitReachedWrapperStyled,
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/Icon/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Check } from './Check'
3 | import { Cross } from './Cross'
4 | import { FormatBold } from './FormatBold'
5 | import { FormatItalic } from './FormatItalic'
6 | import { FormatUnderline } from './FormatUnderline'
7 | import { FormatStriketrough } from './FormatStriketrough'
8 | import { FormatCode } from './FormatCode'
9 | import { BlockText } from './BlockText'
10 | import { BlockH1 } from './BlockH1'
11 | import { BlockH2 } from './BlockH2'
12 | import { BlockH3 } from './BlockH3'
13 | import { BlockNumList } from './BlockNumList'
14 | import { BlockBulletList } from './BlockBulletList'
15 | import { Chevron } from './Chevron'
16 | import { Menu } from './Menu'
17 | import { Bucket } from './Bucket'
18 | import { Exit } from './Exit'
19 | import { TrafficLightOutline } from './TrafficLightOutline'
20 | import { TrafficLightCalendar } from './TrafficLightCalendar'
21 | import { RatingEmoji } from './RatingEmoji'
22 | import { RisedHands } from './RisedHands'
23 | import { UpdateNow } from './UpdateNow'
24 | import { FormatMark } from './FormatMark'
25 | import { FormatHandStriketrough } from './FormatHandStriketrough'
26 | import { Plus } from './Plus'
27 | import { Edit } from './Edit'
28 | import { Trash } from './Trash'
29 | import { Settings } from './Settings'
30 | import { Offline } from './Offline'
31 | import { CardBrand } from './CardBrand'
32 |
33 | type IconMapType = {
34 | [key: string]: any
35 | }
36 |
37 | interface IconProps extends React.HTMLAttributes {
38 | name: keyof IconMapType
39 | size?: number
40 | scale?: number
41 | active?: boolean
42 | tintColor?: any
43 | type?: any
44 | }
45 |
46 | const IconMap: IconMapType = {
47 | Empty,
48 | Check,
49 | Cross,
50 | FormatBold,
51 | FormatItalic,
52 | FormatUnderline,
53 | FormatStriketrough,
54 | FormatHandStriketrough,
55 | FormatCode,
56 | BlockText,
57 | BlockH1,
58 | BlockH2,
59 | BlockH3,
60 | BlockNumList,
61 | BlockBulletList,
62 | Chevron,
63 | Menu,
64 | Bucket,
65 | Exit,
66 | TrafficLightOutline,
67 | TrafficLightCalendar,
68 | RatingEmoji,
69 | RisedHands,
70 | UpdateNow,
71 | FormatMark,
72 | Plus,
73 | Edit,
74 | Trash,
75 | Settings,
76 | Offline,
77 | CardBrand,
78 | }
79 |
80 | const Icon = function (props: IconProps) {
81 | const { name, size, scale, ...rest }: IconProps = props
82 | const Drawing = IconMap[name] ? IconMap[name] : IconMap['Empty']
83 |
84 | return
85 | }
86 |
87 | function Empty() {
88 | return <>>
89 | }
90 |
91 | export { Icon }
92 |
--------------------------------------------------------------------------------
/src/types/subscriptions.ts:
--------------------------------------------------------------------------------
1 | import Stripe from 'stripe'
2 |
3 | interface Product {
4 | id: string /* primary key */
5 | active?: boolean
6 | name?: string
7 | description?: string
8 | image?: string
9 | metadata?: Stripe.Metadata
10 | }
11 |
12 | interface ProductWithPrice extends Product {
13 | prices?: Price[]
14 | }
15 |
16 | interface UserDetails {
17 | id: string /* primary key */
18 | first_name: string
19 | last_name: string
20 | full_name?: string
21 | avatar_url?: string
22 | billing_address?: Stripe.Address
23 | payment_method?: Stripe.PaymentMethod[Stripe.PaymentMethod.Type]
24 | }
25 |
26 | interface Price {
27 | id: string /* primary key */
28 | product_id?: string /* foreign key to products.id */
29 | active?: boolean
30 | description?: string
31 | unit_amount?: number
32 | currency?: string
33 | type?: Stripe.Price.Type
34 | interval?: Stripe.Price.Recurring.Interval
35 | interval_count?: number
36 | trial_period_days?: number | null
37 | metadata?: Stripe.Metadata
38 | products?: Product
39 | }
40 |
41 | interface PriceWithProduct extends Price {}
42 |
43 | interface Subscription {
44 | id: string /* primary key */
45 | user_id: string
46 | status?: Stripe.Subscription.Status
47 | metadata?: Stripe.Metadata
48 | price_id?: string /* foreign key to prices.id */
49 | quantity?: number
50 | cancel_at_period_end?: boolean
51 | created: string
52 | current_period_start: string
53 | current_period_end: string
54 | ended_at?: string
55 | cancel_at?: string
56 | canceled_at?: string
57 | trial_start?: string
58 | trial_end?: string
59 | prices?: Price
60 | }
61 |
62 | interface Countries {
63 | country_code: string /* primary key */
64 | country_name: string
65 | }
66 |
67 | interface CreateSubscriptionProps {
68 | access_token: string
69 | priceId: string
70 | address?: Stripe.Address
71 | }
72 |
73 | interface AddCardProps {
74 | access_token: string
75 | address: Stripe.Address
76 | }
77 |
78 | interface UpdateSubscriptionProps {
79 | access_token: string
80 | subscriptionId: string
81 | }
82 |
83 | interface PreviewInvoiceProps {
84 | access_token: string
85 | }
86 |
87 | interface BillingInfo {
88 | customer: Stripe.Customer
89 | card: Stripe.PaymentMethod | null
90 | invoices: Stripe.Invoice[]
91 | }
92 |
93 | export {
94 | Product,
95 | Price,
96 | Subscription,
97 | Countries,
98 | CreateSubscriptionProps,
99 | UpdateSubscriptionProps,
100 | PreviewInvoiceProps,
101 | AddCardProps,
102 | BillingInfo,
103 | }
104 |
--------------------------------------------------------------------------------
/src/sql/schema.2.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- Create a table for Users
2 | create table
3 | users (
4 | id text,
5 | full_name text,
6 | secret_key blob,
7 | subscription text,
8 | primary key (id)
9 | );
10 |
11 | -- Create a table for journals_catalog
12 | create table
13 | journals_catalog (
14 | user_id text,
15 | journal_id int default 0,
16 | name text not null default 'Default',
17 | color text,
18 | icon_name text,
19 | icon_url text,
20 | primary key (user_id, journal_id),
21 | foreign key (user_id) references users (id) on delete cascade on update no action
22 | );
23 |
24 | -- Create a table for Journals
25 | create table
26 | journals (
27 | user_id text,
28 | day date,
29 | journal_id int default 0,
30 | created_at datetime,
31 | modified_at datetime,
32 | content text,
33 | revision int not null default 0,
34 | sync_status text not null default 'synced',
35 | primary key (user_id, day, journal_id),
36 | foreign key (user_id) references users (id) on delete cascade on update no action,
37 | foreign key (user_id, journal_id) references journals_catalog (user_id, journal_id) on delete cascade on update no action
38 | );
39 |
40 | -- Create a table for Tags
41 | create table
42 | tags (
43 | user_id text,
44 | journal_id int default 0,
45 | id text unique,
46 | name text,
47 | color text,
48 | created_at datetime,
49 | modified_at datetime,
50 | revision int,
51 | sync_status text default 'synced',
52 | primary key (user_id, journal_id, id),
53 | foreign key (user_id, journal_id) references journals_catalog (user_id, journal_id) on delete cascade on update no action
54 | );
55 |
56 | -- Create a table for Public Journal Tags
57 | create table
58 | entries_tags (
59 | user_id text,
60 | day date,
61 | journal_id int default 0,
62 | tag_id text,
63 | order_no int not null default 0,
64 | created_at datetime,
65 | modified_at datetime,
66 | revision int,
67 | sync_status text default 'synced',
68 | primary key (user_id, day, journal_id, tag_id),
69 | foreign key (tag_id) references tags (id) on delete cascade on update no action
70 | -- foreign key (user_id, day, journal_id) references journals (user_id, day, journal_id) on delete cascade on update no action
71 | );
72 |
73 | -- Create a table for user Preferences
74 | create table
75 | preferences (
76 | user_id text,
77 | item text,
78 | value text,
79 | primary key (user_id, item),
80 | foreign key (user_id) references users (id) on delete cascade on update no action
81 | );
82 |
83 | -- Create a table for app
84 | create table
85 | app (key text not null, value text, primary key (key));
--------------------------------------------------------------------------------
/src/components/Menu/Settings/AddCard/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import { Icon } from 'components'
5 |
6 | const FormStyled = styled.form`
7 | width: 368px;
8 | display: flex;
9 | flex-direction: column;
10 | gap: 16px;
11 | `
12 |
13 | const Title = styled.div`
14 | font-style: normal;
15 | font-weight: 500;
16 | font-size: 18px;
17 | line-height: 28px;
18 | letter-spacing: -0.03em;
19 | `
20 |
21 | const TextStyled = styled.div`
22 | font-weight: 500;
23 | font-size: 14px;
24 | line-height: 24px;
25 | `
26 |
27 | const CheckoutModalStyled = styled.div`
28 | background-color: ${theme('color.popper.surface')};
29 | display: flex;
30 | gap: 16px;
31 | flex-direction: column;
32 | position: relative;
33 | padding: 0;
34 | padding: 40px 32px;
35 | margin: 48px 8px 8px 8px;
36 | border-radius: 8px;
37 | -webkit-app-region: no-drag;
38 | `
39 | const ActionsWrapperStyled = styled.div`
40 | display: flex;
41 | gap: 16px;
42 | height: 40px;
43 | `
44 |
45 | const ButtonStyled = styled.button`
46 | background-color: ${theme('color.popper.main')};
47 | color: ${theme('color.popper.inverted')};
48 | outline: 0;
49 | border: 0;
50 | font-weight: 500;
51 | font-size: 14px;
52 | line-height: 24px;
53 | cursor: pointer;
54 | flex: 1;
55 | display: flex;
56 | justify-content: center;
57 | align-items: center;
58 | padding: 8px 12px;
59 | border-radius: 6px;
60 | width: fit-content;
61 | transition: box-shadow ${theme('animation.time.normal')} ease;
62 | &:hover,
63 | &:focus {
64 | box-shadow: 0 0 0 2px ${theme('color.popper.main', 0.15)};
65 | }
66 | &:disabled {
67 | opacity: 0.6;
68 | cursor: default;
69 | }
70 | `
71 |
72 | const ButtonGhostStyled = styled.button`
73 | font-weight: 400;
74 | font-size: 14px;
75 | line-height: 24px;
76 | padding: 2px 8px;
77 | color: ${theme('color.primary.main')};
78 | border: 0;
79 | border-radius: 6px;
80 | flex: 1;
81 | background: transparent;
82 | cursor: pointer;
83 | outline: 0;
84 | transition: ${theme('animation.time.normal')} ease;
85 | &:focus {
86 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
87 | }
88 | &:hover {
89 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
90 | background-color: ${theme('color.popper.border')};
91 | }
92 | &:disabled {
93 | opacity: 0.6;
94 | cursor: default;
95 | }
96 | `
97 |
98 | export {
99 | TextStyled,
100 | CheckoutModalStyled,
101 | ButtonStyled,
102 | Title,
103 | ActionsWrapperStyled,
104 | ButtonGhostStyled,
105 | FormStyled,
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/ChangeCycle/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import * as Tabs from '@radix-ui/react-tabs'
3 | import styled, { keyframes } from 'styled-components'
4 | import { theme } from 'themes'
5 | import { Icon } from 'components'
6 | import { CardElement } from '@stripe/react-stripe-js'
7 |
8 | const FormStyled = styled.form`
9 | width: 280px;
10 | padding-right: 16px;
11 | display: flex;
12 | flex-direction: column;
13 | gap: 24px;
14 | `
15 |
16 | const TextStyled = styled.div`
17 | font-weight: 500;
18 | font-size: 14px;
19 | line-height: 24px;
20 | `
21 |
22 | const CheckoutModalStyled = styled.div`
23 | background-color: ${theme('color.popper.surface')};
24 | display: flex;
25 | position: relative;
26 | padding: 0;
27 | padding: 40px 40px 32px 32px;
28 | margin: 48px 8px 8px 8px;
29 | border-radius: 8px;
30 | -webkit-app-region: no-drag;
31 | `
32 |
33 | const ButtonStyled = styled.button`
34 | background-color: ${theme('color.popper.main')};
35 | color: ${theme('color.popper.inverted')};
36 | outline: 0;
37 | border: 0;
38 | font-weight: 500;
39 | font-size: 14px;
40 | line-height: 24px;
41 | cursor: pointer;
42 | display: flex;
43 | margin-top: 8px;
44 | padding: 8px 12px;
45 | border-radius: 6px;
46 | width: fit-content;
47 | transition: box-shadow ${theme('animation.time.normal')} ease;
48 | &:hover,
49 | &:focus {
50 | box-shadow: 0 0 0 2px ${theme('color.popper.main', 0.15)};
51 | }
52 | &:disabled {
53 | opacity: 0.6;
54 | cursor: default;
55 | }
56 | `
57 |
58 | const TableStyled = styled.div`
59 | letter-spacing: -0.03em;
60 | font-weight: 400;
61 | font-size: 14px;
62 | line-height: 24px;
63 | color: ${theme('color.popper.main')};
64 | & em {
65 | font-weight: 500;
66 | font-size: 16px;
67 | font-style: normal;
68 | }
69 | `
70 |
71 | interface RowStyledProps {
72 | padding?: string
73 | }
74 |
75 | const RowStyled = styled.div`
76 | display: flex;
77 | padding: ${(props) => (props.padding ? props.padding : '0')};
78 | `
79 |
80 | interface CellFillStyledProps {
81 | opacity?: string
82 | }
83 |
84 | const CellFillStyled = styled.div`
85 | flex: 1;
86 | opacity: ${(props) => (props.opacity ? props.opacity : '0.8')};
87 | `
88 |
89 | const Divider = styled.div`
90 | height: 1px;
91 | margin: 8px 0;
92 | background-color: ${theme('color.popper.border')};
93 | `
94 |
95 | const CellStyled = styled.div``
96 |
97 | export {
98 | TextStyled,
99 | CheckoutModalStyled,
100 | ButtonStyled,
101 | RowStyled,
102 | CellFillStyled,
103 | CellStyled,
104 | TableStyled,
105 | Divider,
106 | FormStyled,
107 | }
108 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-text-100: rgba(55, 55, 55, 1);
3 | --color-text-50: #666666;
4 | --color-main-surface: #e5e5e5;
5 | --prompt-opacity: 1;
6 | }
7 |
8 | @font-face {
9 | font-family: 'Inter var';
10 | font-weight: 100 900;
11 | font-display: swap;
12 | font-style: oblique 0deg 10deg;
13 | src: url('../assets/fonts/Inter.var.woff2?v=3.19') format('woff2');
14 | }
15 |
16 | @font-face {
17 | font-family: 'Novela';
18 | src: url('../assets/fonts/novela-regular-webfont.woff2') format('woff2');
19 | font-weight: normal;
20 | font-style: normal;
21 | }
22 |
23 | @font-face {
24 | font-family: 'Novela';
25 | src: url('../assets/fonts/novela-regularitalic-webfont.woff2') format('woff2');
26 | font-weight: normal;
27 | font-style: italic;
28 | }
29 |
30 | @font-face {
31 | font-family: 'Novela';
32 | src: url('../assets/fonts/novela-bold-webfont.woff2') format('woff2');
33 | font-weight: bold;
34 | font-style: normal;
35 | }
36 |
37 | @font-face {
38 | font-family: 'Novela';
39 | src: url('../assets/fonts/novela-bolditalic-webfont.woff2') format('woff2');
40 | font-weight: bold;
41 | font-style: italic;
42 | }
43 |
44 | body {
45 | margin: 0;
46 | padding: 0;
47 | }
48 |
49 | html {
50 | font-family: 'Inter var', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
51 | sans-serif;
52 | font-weight: 400;
53 | font-size: 16px;
54 | font-style: normal;
55 | line-height: 28px;
56 | }
57 |
58 | button,
59 | textarea,
60 | input {
61 | font-family: 'Inter var', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
62 | sans-serif;
63 | }
64 |
65 | strong {
66 | font-weight: 700;
67 | }
68 |
69 | @keyframes react-loading-skeleton {
70 | 100% {
71 | transform: translateX(100%);
72 | }
73 | }
74 |
75 | .react-loading-skeleton {
76 | --base-color: #ebebeb;
77 | --highlight-color: #f5f5f5;
78 | --animation-duration: 1.5s;
79 | --animation-direction: normal;
80 | --pseudo-element-display: block; /* Enable animation */
81 |
82 | background-color: var(--base-color);
83 |
84 | width: 100%;
85 | border-radius: 0.25rem;
86 | display: inline-flex;
87 | line-height: 1;
88 |
89 | position: relative;
90 | overflow: hidden;
91 | z-index: 1; /* Necessary for overflow: hidden to work correctly in Safari */
92 | }
93 |
94 | .react-loading-skeleton::after {
95 | content: ' ';
96 | display: var(--pseudo-element-display);
97 | position: absolute;
98 | top: 0;
99 | left: 0;
100 | right: 0;
101 | height: 100%;
102 | background-repeat: no-repeat;
103 | background-image: linear-gradient(
104 | 90deg,
105 | var(--base-color),
106 | var(--highlight-color),
107 | var(--base-color)
108 | );
109 | transform: translateX(-100%);
110 |
111 | animation-name: react-loading-skeleton;
112 | animation-direction: var(--animation-direction);
113 | animation-duration: var(--animation-duration);
114 | animation-timing-function: ease-in-out;
115 | animation-iteration-count: infinite;
116 | }
117 |
--------------------------------------------------------------------------------
/src/components/Entry/EntryMenu/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { theme } from 'themes'
3 | import { logger } from 'utils'
4 | import {
5 | useFloating,
6 | FloatingOverlay,
7 | useInteractions,
8 | useDismiss,
9 | useClick,
10 | FloatingFocusManager,
11 | useFloatingNodeId,
12 | FloatingNode,
13 | FloatingPortal,
14 | } from '@floating-ui/react-dom-interactions'
15 | import {
16 | IconCloseStyled,
17 | ModalStyled,
18 | ButtonDestructiveStyled,
19 | ButtonStyled,
20 | ButtonGhostStyled,
21 | TitleStyled,
22 | DescriptionStyled,
23 | ActionsWrapperStyled,
24 | } from './styled'
25 |
26 | interface ModalProps {
27 | setOpenModal: React.Dispatch>
28 | action: any
29 | }
30 |
31 | const Modal = ({ setOpenModal, action }: ModalProps) => {
32 | const [open, setOpen] = useState(true)
33 | const actionRef = useRef(null)
34 | const nodeId = useFloatingNodeId()
35 |
36 | const { reference, floating, context, refs } = useFloating({
37 | open,
38 | onOpenChange: setOpen,
39 | nodeId,
40 | })
41 |
42 | const { getReferenceProps, getFloatingProps } = useInteractions([
43 | useClick(context),
44 | useDismiss(context, {
45 | escapeKey: false,
46 | }),
47 | ])
48 |
49 | const handleCloseEsc = (e: any) => {
50 | if (e.key == 'Escape') {
51 | if (refs.floating.current && refs.floating.current.contains(document.activeElement)) {
52 | setOpenModal(false)
53 | }
54 | }
55 | }
56 |
57 | useEffect(() => {
58 | logger('✅ addEventListener')
59 | document.addEventListener('keydown', handleCloseEsc)
60 | setTimeout(() => {
61 | actionRef.current.focus()
62 | }, 200)
63 |
64 | return () => {
65 | logger('❌ removeEventListener')
66 | document.removeEventListener('keydown', handleCloseEsc)
67 | }
68 | }, [])
69 |
70 | return (
71 |
72 |
73 |
82 |
83 |
84 | Remove this entry permanently?
85 | This can't be undone.
86 |
87 | action()}>Remove
88 | setOpenModal(false)}>
89 | Cancel
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
100 | export { Modal }
101 |
--------------------------------------------------------------------------------
/src/sql/migration.1-to-2.sql:
--------------------------------------------------------------------------------
1 | -- Create a table for journals_catalog
2 | create table if not exists
3 | journals_catalog (
4 | user_id text,
5 | journal_id int default 0,
6 | name text not null default 'Default',
7 | color text,
8 | icon_name text,
9 | icon_url text,
10 | primary key (user_id, journal_id),
11 | foreign key (user_id) references users (id) on delete cascade on update no action
12 | );
13 |
14 | -- Create default journal catalog (journal_id=0) for existing users
15 | insert into
16 | journals_catalog (user_id)
17 | select
18 | id
19 | from
20 | users
21 | where
22 | true on conflict (user_id, journal_id)
23 | do nothing;
24 |
25 | -- update table journals to enable multiple journals per user
26 | -- In SQLite, you can not use the ALTER TABLE statement to drop a primary key.
27 | -- Instead, you must create a new table with the primary key removed and copy the data into this new table.
28 | pragma foreign_keys = off;
29 |
30 | begin transaction;
31 |
32 | alter table journals
33 | rename to old_journals;
34 |
35 | create table
36 | journals (
37 | user_id text,
38 | day date,
39 | journal_id int default 0,
40 | created_at datetime,
41 | modified_at datetime,
42 | content text,
43 | revision int not null default 0,
44 | sync_status text not null default 'synced',
45 | primary key (user_id, day, journal_id),
46 | foreign key (user_id) references users (id) on delete cascade on update no action,
47 | foreign key (user_id, journal_id) references journals_catalog (user_id, journal_id) on delete cascade on update no action
48 | );
49 |
50 | insert into
51 | journals (user_id, day, created_at, modified_at, content)
52 | select
53 | user_id,
54 | day,
55 | created_at,
56 | modified_at,
57 | content
58 | from
59 | old_journals;
60 |
61 | commit;
62 |
63 | pragma foreign_keys = on;
64 |
65 | -- Create a table for Tags
66 | create table if not exists
67 | tags (
68 | user_id text,
69 | journal_id int default 0,
70 | id text unique,
71 | name text,
72 | color text,
73 | created_at datetime,
74 | modified_at datetime,
75 | revision int,
76 | sync_status text default 'synced',
77 | primary key (user_id, journal_id, id),
78 | foreign key (user_id, journal_id) references journals_catalog (user_id, journal_id) on delete cascade on update no action
79 | );
80 |
81 | -- Create a table for Public Journal Tags
82 | create table if not exists
83 | entries_tags (
84 | user_id text,
85 | day date,
86 | journal_id int default 0,
87 | tag_id text,
88 | order_no int not null default 0,
89 | created_at datetime,
90 | modified_at datetime,
91 | revision int,
92 | sync_status text default 'synced',
93 | primary key (user_id, day, journal_id, tag_id),
94 | foreign key (tag_id) references tags (id) on delete cascade on update no action
95 | -- foreign key (user_id, day, journal_id) references journals (user_id, day, journal_id) on delete cascade on update no action
96 | );
97 |
98 | -- Add subscription column to cache it
99 | alter table users
100 | add subscription text;
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Billing/PaymentMethod.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { useQuery } from '@tanstack/react-query'
3 | import { loadStripe } from '@stripe/stripe-js/pure'
4 | import { Elements } from '@stripe/react-stripe-js'
5 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
6 | import { Icon } from 'components'
7 | import { theme } from 'themes'
8 | import { AddCard } from '../AddCard'
9 | import { logger, capitalize, stripeEpochToDate, isDev } from 'utils'
10 | import {
11 | HeaderStyled,
12 | TextStyled,
13 | ActionsStyled,
14 | ActionStyled,
15 | ContentStyled,
16 | CardStyled,
17 | ReceiptsRowStyled,
18 | } from './styled'
19 | import type { PaymentMethodProps } from './types'
20 | import type { BillingInfo } from 'types'
21 |
22 | const PaymentMethod = ({ billingInfo, isLoading, showCardOnly = false }: PaymentMethodProps) => {
23 | const [stripePromise, setStripePromise] = useState(null)
24 | const card = billingInfo?.card
25 |
26 | useQuery({
27 | queryKey: ['stripePromise'],
28 | queryFn: async () => {
29 | const url = isDev() ? 'https://s.journal.local' : 'https://s.journal.do'
30 | const { publishableKey } = await fetch(`${url}/api/v1/config`).then((r) => r.json())
31 | setStripePromise(() => loadStripe(publishableKey))
32 | return publishableKey
33 | },
34 | })
35 |
36 | const Card = () => {
37 | if (card) {
38 | const expire = card.card.exp_month + '/' + card.card.exp_year.toString().substring(2)
39 | const brand = capitalize(card.card.brand)
40 | const last4 = card.card.last4
41 | return (
42 |
43 |
44 |
45 | {brand} **** {last4}
46 |
47 | expiring {expire}
48 |
49 | )
50 | }
51 | return No card added
52 | }
53 |
54 | return (
55 |
56 | {!showCardOnly && Payment method}
57 |
58 | {isLoading ? : }
59 | {!showCardOnly &&
60 | (isLoading ? (
61 |
62 | ) : card ? (
63 |
64 |
65 | (
68 |
69 | Update
70 |
71 | )}
72 | />
73 |
74 |
75 | ) : (
76 |
77 |
78 | (
80 |
81 | Add card
82 |
83 | )}
84 | />
85 |
86 |
87 | ))}
88 |
89 |
90 | )
91 | }
92 |
93 | export { PaymentMethod }
94 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Billing/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled from 'styled-components'
3 | import { theme } from 'themes'
4 |
5 | const HeaderStyled = styled.div`
6 | font-weight: 500;
7 | font-size: 16px;
8 | line-height: 24px;
9 | letter-spacing: -0.03em;
10 | color: ${theme('color.popper.main')};
11 | margin-bottom: 8px;
12 | margin-top: 32px;
13 | `
14 |
15 | const TextStyled = styled.div`
16 | flex-grow: 1;
17 | font-weight: 400;
18 | font-size: 12px;
19 | line-height: 20px;
20 | & em {
21 | font-weight: 500;
22 | font-style: normal;
23 | }
24 | & strong {
25 | font-weight: 500;
26 | font-size: 14px;
27 | line-height: 24px;
28 | }
29 | `
30 |
31 | const ActionsStyled = styled.div`
32 | display: flex;
33 | flex-wrap: wrap;
34 | justify-content: end;
35 | align-self: flex-start;
36 | row-gap: 2px;
37 | gap: 4px;
38 | `
39 | type ActionStyledProps = {
40 | isHidden?: boolean
41 | }
42 |
43 | const ActionStyled = styled.button`
44 | ${(props) => (props.isHidden ? 'display: none;' : '')};
45 | font-weight: 400;
46 | font-size: 12px;
47 | line-height: 15px;
48 | white-space: nowrap;
49 | text-align: right;
50 | padding: 4px 6px;
51 | height: fit-content;
52 | color: ${theme('color.primary.main')};
53 | opacity: 0.8;
54 | border: 0;
55 | border-radius: 100px;
56 | background: transparent;
57 | cursor: pointer;
58 | outline: 0;
59 | transition: ${theme('animation.time.normal')};
60 | &:hover,
61 | &:focus {
62 | background-color: ${theme('color.popper.hover')};
63 | opacity: 1;
64 | }
65 | `
66 |
67 | const ContentStyled = styled.div`
68 | display: flex;
69 | `
70 |
71 | const CardStyled = styled.div`
72 | display: flex;
73 | align-items: center;
74 | gap: 8px;
75 | & em {
76 | font-weight: 500;
77 | font-style: normal;
78 | }
79 | `
80 |
81 | const ReceiptsTableStyled = styled.div`
82 | &:first-child {
83 | border-top: 1px solid ${theme('color.popper.border')};
84 | }
85 | > * {
86 | border-bottom: 1px solid ${theme('color.popper.border')};
87 | }
88 | `
89 |
90 | const ReceiptsRowStyled = styled.div`
91 | display: flex;
92 | padding: 8px 0;
93 | gap: 16px;
94 | `
95 |
96 | const ReceiptsCellStyled = styled.div`
97 | &:first-child {
98 | flex-grow: 1;
99 | }
100 | `
101 |
102 | const DownloadStyled = styled.a`
103 | font-weight: 400;
104 | font-size: 12px;
105 | line-height: 15px;
106 | padding: 4px 6px;
107 | color: ${theme('color.primary.main')};
108 | opacity: 0.8;
109 | border: 0;
110 | border-radius: 100px;
111 | background: transparent;
112 | text-decoration: none;
113 | cursor: pointer;
114 | outline: 0;
115 | transition: ${theme('animation.time.normal')};
116 | &:hover,
117 | &:focus {
118 | background-color: ${theme('color.popper.hover')};
119 | opacity: 1;
120 | }
121 | `
122 |
123 | const Divider = styled.div`
124 | background-color: ${theme('color.popper.border')};
125 | height: 1px;
126 | margin: 32px 0;
127 | `
128 |
129 | export {
130 | HeaderStyled,
131 | TextStyled,
132 | ActionsStyled,
133 | ActionStyled,
134 | ContentStyled,
135 | CardStyled,
136 | ReceiptsRowStyled,
137 | ReceiptsTableStyled,
138 | ReceiptsCellStyled,
139 | Divider,
140 | DownloadStyled,
141 | }
142 |
--------------------------------------------------------------------------------
/src/components/FeedbackWidget/RatingEmojiControl.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { Icon } from 'components'
4 |
5 | interface RatingEmojiContainerProps {
6 | borderRadius?: string
7 | }
8 |
9 | const RatingEmojiContainer = styled.div`
10 | display: flex;
11 | align-items: center;
12 | gap: 8px;
13 | justify-content: space-evenly;
14 | border: 0;
15 | border-radius: ${(props) => props.borderRadius || '0'};
16 | box-sizing: border-box;
17 | padding: 24px;
18 | width: 100%;
19 | margin: 0 0 1px 0;
20 | & svg {
21 | transition: 0.1s ease-out;
22 | padding: 8px;
23 | }
24 | `
25 |
26 | const emojiJump = keyframes`
27 | 0% {
28 | transform: scale(1.4);
29 | }
30 | 50% {
31 | transform: scale(1.5) translateY(-10px);
32 | }
33 | 100% {
34 | transform: scale(1.4);
35 | }
36 | `
37 |
38 | const IconEmoji = styled((props) => )`
39 | cursor: pointer;
40 | &:focus {
41 | outline: none;
42 | animation-name: ${emojiJump};
43 | animation-duration: 0.4s;
44 | animation-timing-function: cubic-bezier(0.17, 0.18, 0.41, 0.99);
45 | animation-fill-mode: both;
46 | }
47 | `
48 |
49 | type RatingEmojiControlProps = {
50 | onClickFunc: (type: string) => void
51 | shouldReset: boolean
52 | // cachedEntry?: any
53 | // ref?: any
54 | // setEntryHeight: (id: string, height: number) => void
55 | // setCachedEntry: (property: string, value: any) => void
56 | // shouldScrollToDay: (day: string) => boolean
57 | // clearScrollToDay: () => void
58 | }
59 |
60 | function RatingEmojiControl({ onClickFunc, shouldReset }: RatingEmojiControlProps) {
61 | const emojis = ['angry', 'thinking', 'neutral', 'happy', 'love']
62 |
63 | const [hover, setHover] = useState('')
64 | const [selected, setSelected] = useState('')
65 |
66 | function whatStyle(type: string) {
67 | if (hover) {
68 | if (hover == type) {
69 | return { transform: 'scale(1.4)' }
70 | } else {
71 | return { transform: 'scale(1)' }
72 | }
73 | }
74 | if (selected) {
75 | if (selected == type) {
76 | return { transform: 'scale(1.4)' }
77 | } else {
78 | return { opacity: '0.5' }
79 | }
80 | }
81 | }
82 |
83 | function onClick(type: string) {
84 | setSelected(type)
85 | onClickFunc(type)
86 | }
87 |
88 | function onKeyPress(e: React.KeyboardEvent, type: string) {
89 | if (e.key == 'Enter') {
90 | setSelected(type)
91 | onClickFunc(type)
92 | }
93 | }
94 |
95 | useEffect(() => {
96 | setSelected('')
97 | }, [shouldReset])
98 |
99 | return (
100 |
101 | {/* {console.count('Render count:')} */}
102 | {emojis.map((type, i) => (
103 | onClick(type)}
109 | onKeyPress={(e: any) => onKeyPress(e, type)}
110 | onMouseEnter={() => setHover(type)}
111 | onMouseLeave={() => setHover('')}
112 | key={`emojiRating-${i}`}
113 | />
114 | ))}
115 |
116 | )
117 | }
118 |
119 | export { RatingEmojiControl }
120 |
--------------------------------------------------------------------------------
/src/components/Entry/EntryMenu/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import { Icon } from 'components'
5 |
6 | const IconCloseStyled = styled((props) => )`
7 | position: absolute;
8 | top: 16px;
9 | right: 16px;
10 | opacity: 0.8;
11 | cursor: pointer;
12 | &:hover {
13 | opacity: 1;
14 | color: ${theme('color.primary.main')};
15 | }
16 | `
17 |
18 | const ModalStyled = styled.div`
19 | background-color: ${theme('color.popper.surface')};
20 | width: 330px;
21 | display: flex;
22 | box-sizing: border-box;
23 | flex-direction: column;
24 | position: relative;
25 | padding: 0;
26 | padding: 24px 24px 16px 24px;
27 | margin: 48px 8px 8px 8px;
28 | border-radius: 12px;
29 | -webkit-app-region: no-drag;
30 | `
31 |
32 | const ButtonDestructiveStyled = styled.button`
33 | font-weight: 500;
34 | font-size: 14px;
35 | line-height: 24px;
36 | padding: 2px 8px;
37 | color: ${theme('color.error.main')};
38 | border: 1px solid ${theme('color.error.main')};
39 | border-radius: 6px;
40 | flex: 1;
41 | background: transparent;
42 | cursor: pointer;
43 | outline: 0;
44 | &:hover,
45 | &:focus {
46 | box-shadow: 0 0 0 3px ${theme('color.error.main', 0.2)};
47 | transition: box-shadow ${theme('animation.time.normal')} ease;
48 | }
49 | &:disabled {
50 | opacity: 0.6;
51 | cursor: default;
52 | }
53 | `
54 |
55 | const ButtonStyled = styled.button`
56 | background-color: ${theme('color.popper.main')};
57 | color: ${theme('color.popper.inverted')};
58 | outline: 0;
59 | border: 0;
60 | font-weight: 500;
61 | font-size: 14px;
62 | line-height: 24px;
63 | cursor: pointer;
64 | padding: 2px 8px;
65 | border-radius: 6px;
66 | flex: 1;
67 | transition: box-shadow ${theme('animation.time.normal')} ease;
68 | &:hover,
69 | &:focus {
70 | box-shadow: 0 0 0 3px ${theme('color.popper.main', 0.15)};
71 | }
72 | &:disabled {
73 | opacity: 0.6;
74 | cursor: default;
75 | }
76 | `
77 |
78 | const ButtonGhostStyled = styled.button`
79 | font-weight: 400;
80 | font-size: 14px;
81 | line-height: 24px;
82 | padding: 2px 8px;
83 | color: ${theme('color.primary.main')};
84 | border: 0;
85 | border-radius: 6px;
86 | flex: 1;
87 | background: transparent;
88 | cursor: pointer;
89 | outline: 0;
90 | transition: ${theme('animation.time.normal')} ease;
91 | &:focus {
92 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
93 | }
94 | &:hover {
95 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
96 | background-color: ${theme('color.popper.border')};
97 | }
98 | &:disabled {
99 | opacity: 0.6;
100 | cursor: default;
101 | }
102 | `
103 |
104 | const TitleStyled = styled.div`
105 | font-style: normal;
106 | font-weight: 500;
107 | font-size: 18px;
108 | line-height: 28px;
109 | margin-bottom: 8px;
110 | letter-spacing: -0.03em;
111 | `
112 |
113 | const DescriptionStyled = styled.div`
114 | font-weight: 400;
115 | font-size: 12px;
116 | line-height: 18px;
117 | `
118 |
119 | const ActionsWrapperStyled = styled.div`
120 | display: flex;
121 | gap: 16px;
122 | margin-top: 32px;
123 | height: 38px;
124 | `
125 |
126 | export {
127 | IconCloseStyled,
128 | ModalStyled,
129 | ButtonDestructiveStyled,
130 | ButtonStyled,
131 | ButtonGhostStyled,
132 | TitleStyled,
133 | DescriptionStyled,
134 | ActionsWrapperStyled,
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/CancelImmediately/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import { Icon } from 'components'
5 |
6 | const IconCloseStyled = styled((props) => )`
7 | position: absolute;
8 | top: 16px;
9 | right: 16px;
10 | opacity: 0.8;
11 | cursor: pointer;
12 | &:hover {
13 | opacity: 1;
14 | color: ${theme('color.primary.main')};
15 | }
16 | `
17 |
18 | const ModalStyled = styled.div`
19 | background-color: ${theme('color.popper.surface')};
20 | width: 330px;
21 | display: flex;
22 | box-sizing: border-box;
23 | flex-direction: column;
24 | position: relative;
25 | padding: 0;
26 | padding: 24px 24px 16px 24px;
27 | margin: 48px 8px 8px 8px;
28 | border-radius: 12px;
29 | -webkit-app-region: no-drag;
30 | `
31 |
32 | const ButtonDestructiveStyled = styled.button`
33 | font-weight: 500;
34 | font-size: 14px;
35 | line-height: 24px;
36 | padding: 2px 8px;
37 | color: ${theme('color.error.main')};
38 | border: 1px solid ${theme('color.error.main')};
39 | border-radius: 6px;
40 | flex: 1;
41 | background: transparent;
42 | cursor: pointer;
43 | outline: 0;
44 | &:hover,
45 | &:focus {
46 | box-shadow: 0 0 0 3px ${theme('color.error.main', 0.2)};
47 | transition: box-shadow ${theme('animation.time.normal')} ease;
48 | }
49 | &:disabled {
50 | opacity: 0.6;
51 | cursor: default;
52 | }
53 | `
54 |
55 | const ButtonStyled = styled.button`
56 | background-color: ${theme('color.popper.main')};
57 | color: ${theme('color.popper.inverted')};
58 | outline: 0;
59 | border: 0;
60 | font-weight: 500;
61 | font-size: 14px;
62 | line-height: 24px;
63 | cursor: pointer;
64 | padding: 2px 8px;
65 | border-radius: 6px;
66 | flex: 1;
67 | transition: box-shadow ${theme('animation.time.normal')} ease;
68 | &:hover,
69 | &:focus {
70 | box-shadow: 0 0 0 3px ${theme('color.popper.main', 0.15)};
71 | }
72 | &:disabled {
73 | opacity: 0.6;
74 | cursor: default;
75 | }
76 | `
77 |
78 | const ButtonGhostStyled = styled.button`
79 | font-weight: 400;
80 | font-size: 14px;
81 | line-height: 24px;
82 | padding: 2px 8px;
83 | color: ${theme('color.primary.main')};
84 | border: 0;
85 | border-radius: 6px;
86 | flex: 1;
87 | background: transparent;
88 | cursor: pointer;
89 | outline: 0;
90 | transition: ${theme('animation.time.normal')} ease;
91 | &:focus {
92 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
93 | }
94 | &:hover {
95 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
96 | background-color: ${theme('color.popper.border')};
97 | }
98 | &:disabled {
99 | opacity: 0.6;
100 | cursor: default;
101 | }
102 | `
103 |
104 | const TitleStyled = styled.div`
105 | font-style: normal;
106 | font-weight: 500;
107 | font-size: 18px;
108 | line-height: 28px;
109 | margin-bottom: 8px;
110 | letter-spacing: -0.03em;
111 | `
112 |
113 | const DescriptionStyled = styled.div`
114 | font-weight: 400;
115 | font-size: 12px;
116 | line-height: 18px;
117 | `
118 |
119 | const ActionsWrapperStyled = styled.div`
120 | display: flex;
121 | gap: 16px;
122 | margin-top: 32px;
123 | height: 38px;
124 | `
125 |
126 | export {
127 | IconCloseStyled,
128 | ModalStyled,
129 | ButtonDestructiveStyled,
130 | ButtonStyled,
131 | ButtonGhostStyled,
132 | TitleStyled,
133 | DescriptionStyled,
134 | ActionsWrapperStyled,
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/CancelOrResume/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import { Icon } from 'components'
5 |
6 | const IconCloseStyled = styled((props) => )`
7 | position: absolute;
8 | top: 16px;
9 | right: 16px;
10 | opacity: 0.8;
11 | cursor: pointer;
12 | &:hover {
13 | opacity: 1;
14 | color: ${theme('color.primary.main')};
15 | }
16 | `
17 |
18 | const ModalStyled = styled.div`
19 | background-color: ${theme('color.popper.surface')};
20 | width: 330px;
21 | display: flex;
22 | box-sizing: border-box;
23 | flex-direction: column;
24 | position: relative;
25 | padding: 0;
26 | padding: 24px 24px 16px 24px;
27 | margin: 48px 8px 8px 8px;
28 | border-radius: 12px;
29 | -webkit-app-region: no-drag;
30 | `
31 |
32 | const ButtonDestructiveStyled = styled.button`
33 | font-weight: 500;
34 | font-size: 14px;
35 | line-height: 24px;
36 | padding: 2px 8px;
37 | color: ${theme('color.error.main')};
38 | border: 1px solid ${theme('color.error.main')};
39 | border-radius: 6px;
40 | flex: 1;
41 | background: transparent;
42 | cursor: pointer;
43 | outline: 0;
44 | &:hover,
45 | &:focus {
46 | box-shadow: 0 0 0 3px ${theme('color.error.main', 0.2)};
47 | transition: box-shadow ${theme('animation.time.normal')} ease;
48 | }
49 | &:disabled {
50 | opacity: 0.6;
51 | cursor: default;
52 | }
53 | `
54 |
55 | const ButtonStyled = styled.button`
56 | background-color: ${theme('color.popper.main')};
57 | color: ${theme('color.popper.inverted')};
58 | outline: 0;
59 | border: 0;
60 | font-weight: 500;
61 | font-size: 14px;
62 | line-height: 24px;
63 | cursor: pointer;
64 | padding: 2px 8px;
65 | border-radius: 6px;
66 | flex: 1;
67 | transition: box-shadow ${theme('animation.time.normal')} ease;
68 | &:hover,
69 | &:focus {
70 | box-shadow: 0 0 0 3px ${theme('color.popper.main', 0.15)};
71 | }
72 | &:disabled {
73 | opacity: 0.6;
74 | cursor: default;
75 | }
76 | `
77 |
78 | const ButtonGhostStyled = styled.button`
79 | font-weight: 400;
80 | font-size: 14px;
81 | line-height: 24px;
82 | padding: 2px 8px;
83 | color: ${theme('color.primary.main')};
84 | border: 0;
85 | border-radius: 6px;
86 | flex: 1;
87 | background: transparent;
88 | cursor: pointer;
89 | outline: 0;
90 | transition: ${theme('animation.time.normal')} ease;
91 | &:focus {
92 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
93 | }
94 | &:hover {
95 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
96 | background-color: ${theme('color.popper.border')};
97 | }
98 | &:disabled {
99 | opacity: 0.6;
100 | cursor: default;
101 | }
102 | `
103 |
104 | const TitleStyled = styled.div`
105 | font-style: normal;
106 | font-weight: 500;
107 | font-size: 18px;
108 | line-height: 28px;
109 | margin-bottom: 8px;
110 | letter-spacing: -0.03em;
111 | `
112 |
113 | const DescriptionStyled = styled.div`
114 | font-weight: 400;
115 | font-size: 12px;
116 | line-height: 18px;
117 | `
118 |
119 | const ActionsWrapperStyled = styled.div`
120 | display: flex;
121 | gap: 16px;
122 | margin-top: 32px;
123 | height: 38px;
124 | `
125 |
126 | export {
127 | IconCloseStyled,
128 | ModalStyled,
129 | ButtonDestructiveStyled,
130 | ButtonStyled,
131 | ButtonGhostStyled,
132 | TitleStyled,
133 | DescriptionStyled,
134 | ActionsWrapperStyled,
135 | }
136 |
--------------------------------------------------------------------------------
/schemas/schema.postgresql.sql:
--------------------------------------------------------------------------------
1 | -- Create a table for Public Profiles
2 | create table
3 | profiles (
4 | user_id uuid references auth.users (id) not null,
5 | modified_at timestamp with time zone default now(),
6 | avatar_url text,
7 | full_name text,
8 | primary key (user_id)
9 | );
10 |
11 | alter table
12 | profiles enable row level security;
13 |
14 | create policy
15 | "Users can manipulate only their own profiles" on profiles for insert
16 | with
17 | check (auth.uid () = user_id);
18 |
19 | -- Create a table for Public Journals
20 | create table
21 | journals (
22 | user_id uuid references auth.users (id) not null,
23 | day date not null,
24 | created_at timestamp with time zone default now(),
25 | modified_at timestamp with time zone default now(),
26 | content bytea,
27 | iv bytea,
28 | primary key (user_id, day)
29 | )
30 | partition by
31 | LIST (user_id);
32 |
33 | alter table
34 | journals enable row level security;
35 |
36 | create policy
37 | "Users can manipulate only their own journals" on journals for all using (auth.uid () = user_id);
38 |
39 | create
40 | or replace function create_user_partition () returns trigger as $create_user_partition$
41 | BEGIN
42 | EXECUTE 'CREATE TABLE public.journals_' || replace(new.id::text,'-','_') || ' PARTITION OF public.journals FOR VALUES IN (''' || new.id || ''')';
43 | RETURN NEW;
44 | END;
45 | $create_user_partition$ language plpgsql security definer;
46 |
47 | create trigger
48 | create_user_partition
49 | after
50 | insert on auth.users for each row
51 | execute
52 | function create_user_partition ();
53 |
54 | -- Create partitions for existing users
55 | create
56 | or replace function create_user_partition_for_existing_users (arg uuid[]) returns void as $$
57 | DECLARE
58 | u uuid;
59 | BEGIN
60 | FOREACH u IN ARRAY arg LOOP
61 | EXECUTE 'CREATE TABLE public.journals_' || replace(u::text,'-','_') || ' PARTITION OF public.journals FOR VALUES IN (''' || u || ''')';
62 | END LOOP;
63 | END;
64 | $$ language plpgsql;
65 |
66 | select
67 | create_user_partition_for_existing_users (array_agg(id))
68 | from
69 | auth.users;
70 |
71 | -- Create a table for Public Website pages
72 | create table
73 | website_pages (
74 | id serial unique not null,
75 | page varchar(100) not null,
76 | content text,
77 | primary key (page)
78 | );
79 |
80 | alter table
81 | website_pages enable row level security;
82 |
83 | create policy
84 | "Anyone can read content" on website_pages for
85 | select
86 | using (true);
87 |
88 | -- Create a table for Public Website changelog
89 | create table
90 | releases (
91 | version varchar(50),
92 | pub_date timestamp with time zone not null,
93 | notes text,
94 | primary key (version)
95 | );
96 |
97 | alter table
98 | releases enable row level security;
99 |
100 | create policy
101 | "Anyone can read content" on releases for
102 | select
103 | using (true);
104 |
105 | -- Create a table for user feedback
106 | create table
107 | feedback (
108 | id serial unique not null,
109 | user_id uuid references auth.users (id) not null,
110 | rating int,
111 | check (
112 | rating >= 1
113 | and rating <= 5
114 | ),
115 | feedback text,
116 | created_at timestamp with time zone default now(),
117 | email varchar(100),
118 | primary key (id)
119 | );
120 |
121 | alter table
122 | feedback enable row level security;
123 |
124 | create policy
125 | "Only Authenticated users can add feedback" on feedback for insert
126 | with
127 | check (auth.uid () = user_id);
--------------------------------------------------------------------------------
/src/config/UserPreferences.ts:
--------------------------------------------------------------------------------
1 | import { lightTheme, darkTheme, forestTheme, cappuccinoTheme } from 'themes'
2 |
3 | const fontSizeMap = {
4 | small: 18,
5 | normal: 21,
6 | large: 23,
7 | }
8 | type FontSize = keyof typeof fontSizeMap
9 |
10 | const fontFaceMap = {
11 | inter: 'Inter var',
12 | novela: 'Novela',
13 | }
14 | type FontFace = keyof typeof fontFaceMap
15 |
16 | const colorThemeMap = {
17 | light: lightTheme,
18 | dark: darkTheme,
19 | forest: forestTheme,
20 | cappuccino: cappuccinoTheme,
21 | }
22 | type ColorTheme = keyof typeof colorThemeMap
23 |
24 | const spellCheckMap = {
25 | true: 'true',
26 | false: 'false',
27 | }
28 | type SpellCheckEnabled = keyof typeof spellCheckMap
29 |
30 | const calendarOpenMap = {
31 | opened: { entriesOffset: 200, miniDatesVisibility: 'visible' },
32 | closed: { entriesOffset: 0, miniDatesVisibility: 'hidden' },
33 | }
34 | type CalendarOpen = keyof typeof calendarOpenMap
35 |
36 | type PromptsOpen = 'opened' | 'closed'
37 |
38 | type PromptSelectedId = number
39 |
40 | const defaultUserPreferences = {
41 | fontSize: 'normal' as FontSize,
42 | fontFace: 'inter' as FontFace,
43 | theme: 'light' as ColorTheme,
44 | calendarOpen: 'closed' as CalendarOpen,
45 | spellCheckEnabled: 'true' as SpellCheckEnabled,
46 | promptsOpen: 'closed' as PromptsOpen,
47 | promptSelectedId: 1 as PromptSelectedId,
48 | }
49 |
50 | const baseTheme = {
51 | appearance: {
52 | fontFace: 'Inter var',
53 | fontSize: '21px',
54 | entriesOffset: '0',
55 | miniDatesVisibility: 'hidden',
56 | },
57 | animation: {
58 | time: {
59 | veryFast: '50ms',
60 | fast: '100ms',
61 | normal: '200ms',
62 | long: '400ms',
63 | },
64 | timingFunction: {
65 | dynamic: 'cubic-bezier(0.31, 0.3, 0.17, 0.99)',
66 | },
67 | },
68 | }
69 |
70 | const getFontSize = (name: FontSize) => {
71 | return fontSizeMap[name] ? fontSizeMap[name] : fontSizeMap['normal']
72 | }
73 |
74 | const getFontFace = (name: FontFace) => {
75 | return fontFaceMap[name] ? fontFaceMap[name] : fontFaceMap['inter']
76 | }
77 |
78 | const getColorTheme = (name: ColorTheme) => {
79 | return colorThemeMap[name] ? colorThemeMap[name] : colorThemeMap['light']
80 | }
81 |
82 | const getSpellCheckIsEnabled = (name: SpellCheckEnabled) => {
83 | return spellCheckMap[name] ? spellCheckMap[name] : spellCheckMap['true']
84 | }
85 |
86 | const getCalendarIsOpen = (state: CalendarOpen) => {
87 | return calendarOpenMap[state] ? calendarOpenMap[state] : calendarOpenMap['closed']
88 | }
89 |
90 | const getBaseThemeWithOverrides = (overrides: any) => {
91 | let theme = { ...baseTheme }
92 |
93 | if (overrides && overrides.fontFace) {
94 | theme.appearance.fontFace = getFontFace(overrides.fontFace)
95 | }
96 |
97 | if (overrides && overrides.fontSize) {
98 | theme.appearance.fontSize = getFontSize(overrides.fontSize) + 'px'
99 | }
100 |
101 | if (overrides && overrides.calendarOpen) {
102 | theme.appearance.entriesOffset = getCalendarIsOpen(overrides.calendarOpen).entriesOffset + 'px'
103 | theme.appearance.miniDatesVisibility = getCalendarIsOpen(
104 | overrides.calendarOpen
105 | ).miniDatesVisibility
106 | }
107 |
108 | return theme
109 | }
110 |
111 | export {
112 | baseTheme,
113 | getBaseThemeWithOverrides,
114 | getFontSize,
115 | getFontFace,
116 | getColorTheme,
117 | getCalendarIsOpen,
118 | getSpellCheckIsEnabled,
119 | defaultUserPreferences,
120 | ColorTheme,
121 | FontSize,
122 | FontFace,
123 | CalendarOpen,
124 | SpellCheckEnabled,
125 | PromptsOpen,
126 | PromptSelectedId,
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/EntryList/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { EntryItem } from 'components'
3 | import { useEventEditorSelectors } from '@udecode/plate'
4 | import { supabase, arrayEquals, isUnauthorized, logger } from 'utils'
5 | import { useEntriesContext, useUserContext } from 'context'
6 | import dayjs from 'dayjs'
7 | import { BeforeEntries, PostEntries, Wrapper } from './styled'
8 | import type { Day, Tag } from 'types'
9 |
10 | var visibleSections: String[] = []
11 | var rangeMarker: any
12 | var calendarContainer: any
13 | var scrollToToday: any
14 | var rangeMarkerTop: number
15 |
16 | const renderScrollToToday = () => {
17 | if (!scrollToToday) {
18 | scrollToToday = document.getElementById('ScrollToToday')
19 | }
20 | let today = dayjs().format('YYYY-MM-DD')
21 | if (visibleSections.some((day) => day == today)) {
22 | scrollToToday.style.marginBottom = '-32px'
23 | } else {
24 | scrollToToday.style.marginBottom = 0
25 | }
26 | }
27 |
28 | const renderMarker = () => {
29 | visibleSections.sort()
30 |
31 | if (!rangeMarker) {
32 | rangeMarker = document.getElementById('RangeVisible')
33 | }
34 |
35 | visibleSections.forEach((date: string, i) => {
36 | let elem = document.getElementById(`${date}-calendar`)
37 | if (elem) {
38 | const top = elem.offsetTop
39 |
40 | if (i == 0) {
41 | rangeMarkerTop = top
42 | rangeMarker.style.top = rangeMarkerTop - 2 + 'px'
43 | }
44 |
45 | if (i == visibleSections.length - 1) {
46 | let height = top - rangeMarkerTop + elem.offsetHeight + 4 + 'px'
47 | rangeMarker.style.height = height
48 | }
49 | }
50 | })
51 | }
52 |
53 | const onIntersection = (entries: any) => {
54 | entries.forEach((entry: any) => {
55 | let date = entry.target.id.slice(0, 10)
56 | if (entry.isIntersecting) {
57 | // Add to array
58 | visibleSections.push(date)
59 | } else {
60 | // Remove from array
61 | visibleSections = visibleSections.filter((v) => {
62 | return v != date
63 | })
64 | }
65 | renderMarker()
66 | renderScrollToToday()
67 | })
68 |
69 | if (!calendarContainer) {
70 | calendarContainer = document.getElementById('CalendarContainer')
71 | }
72 |
73 | calendarContainer.scrollTo({
74 | top: rangeMarkerTop - 48,
75 | behavior: 'smooth',
76 | })
77 | }
78 |
79 | const entriesObserver = new IntersectionObserver(onIntersection, {
80 | rootMargin: '-100px',
81 | })
82 |
83 | const EntryMemo = React.memo(EntryItem, (prevProps, nextProps) => {
84 | logger('New memo compare')
85 | if (prevProps.entryDay === nextProps.entryDay) {
86 | return true
87 | }
88 | return false
89 | })
90 |
91 | function EntryList() {
92 | const [days, setDaysInternal] = useState([])
93 | const { userEntries, invokeRerenderEntryList } = useEntriesContext()
94 |
95 | const setDays = () => {
96 | let today = dayjs().format('YYYY-MM-DD') as Day
97 | let daysInCache = userEntries.current.map((entry) => entry.day) as Day[]
98 | setDaysInternal([...new Set([...daysInCache, today])].sort())
99 | }
100 |
101 | useEffect(() => {
102 | invokeRerenderEntryList.current = setDays
103 | }, [])
104 |
105 | return (
106 |
107 |
108 | {days
109 | .slice(0)
110 | .reverse()
111 | .map((day, i) => (
112 | item.day == day)}
117 | />
118 | ))}
119 |
120 |
121 | )
122 | }
123 |
124 | export { EntryList }
125 |
--------------------------------------------------------------------------------
/src/components/Entry/EntryAside.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 | import { theme } from 'themes'
3 | import styled from 'styled-components'
4 | import dayjs from 'dayjs'
5 | import { ordinal, breakpoints, logger, arrayEquals } from 'utils'
6 | import { Icon, EntryTags } from 'components'
7 | import { useEntriesContext } from 'context'
8 | import { EntryMenu } from './EntryMenu'
9 |
10 | const AsideDay = styled.p`
11 | padding: 0;
12 | margin: 0;
13 | color: ${theme('color.primary.main')};
14 | opacity: 0.3;
15 | font-size: 16px;
16 | font-weight: 500;
17 | line-height: 24px;
18 | `
19 |
20 | const AsideYear = styled.p`
21 | padding: 0;
22 | margin: 0;
23 | color: ${theme('color.primary.main')};
24 | opacity: 0.3;
25 | font-size: 12px;
26 | font-weight: 300;
27 | line-height: 20px;
28 | `
29 |
30 | const AsideMeta = styled.div`
31 | top: 0;
32 | right: 0;
33 | text-align: -webkit-right;
34 | transition: ${theme('animation.time.normal')};
35 | padding-top: 24px;
36 | `
37 |
38 | const AsideMain = styled.div`
39 | transition: ${theme('animation.time.normal')};
40 | `
41 |
42 | const AsideStickyContainer = styled.div`
43 | position: sticky;
44 | top: 48px;
45 | text-align: end;
46 | `
47 |
48 | const Aside = styled.div`
49 | -webkit-app-region: no-drag;
50 | width: 160px;
51 | padding-top: 24px;
52 | display: flex;
53 | flex-direction: column;
54 | @media ${breakpoints.s} {
55 | display: none;
56 | }
57 | `
58 |
59 | const AsideMenu = styled.div`
60 | width: 40px;
61 | padding-top: 24px;
62 | padding-bottom: 6px;
63 | display: flex;
64 | flex-direction: column;
65 | transition: padding ${theme('animation.time.normal')};
66 | @media ${breakpoints.s} {
67 | display: none;
68 | }
69 | @media ${breakpoints.xl} {
70 | padding-right: var(--appearance-entriesOffset);
71 | }
72 | `
73 |
74 | const AsideMenuStickyContainer = styled.div`
75 | position: sticky;
76 | -webkit-app-region: no-drag;
77 | top: 48px;
78 | display: flex;
79 | justify-content: center;
80 | `
81 |
82 | const Trigger = styled((props) => )`
83 | -webkit-app-region: no-drag;
84 | opacity: 0.5;
85 | transition: opacity ${theme('animation.time.normal')};
86 | `
87 |
88 | const isToday = (day: any) => {
89 | return day.toString() == dayjs().format('YYYY-MM-DD')
90 | }
91 |
92 | const showDate = (day: any) => {
93 | if (isToday(day)) {
94 | return (
95 | <>
96 | Today
97 | {dayjs(dayjs(day.toString(), 'YYYY-MM-DD')).format('D MMMM YYYY')}
98 | >
99 | )
100 | } else {
101 | return (
102 | <>
103 | {dayjs(dayjs(day.toString(), 'YYYY-MM-DD')).format('D MMMM')}
104 | {dayjs(dayjs(day.toString(), 'YYYY-MM-DD')).format('YYYY')}
105 | >
106 | )
107 | }
108 | }
109 |
110 | type EntryAsideProps = {
111 | date: string
112 | wordCount: React.MutableRefObject
113 | freePlanLimitReached: boolean
114 | }
115 |
116 | function EntryAside({ date, wordCount, freePlanLimitReached }: EntryAsideProps) {
117 | return (
118 | <>
119 |
125 |
126 |
127 | }
131 | />
132 |
133 |
134 | >
135 | )
136 | }
137 |
138 | export { EntryAside }
139 |
--------------------------------------------------------------------------------
/schemas/migration2.postgres.sql:
--------------------------------------------------------------------------------
1 | --------------------------------------------
2 | -- Fix RLS for journals and entries_tags
3 | --------------------------------------------
4 | -- Create RLS for existing users on journals
5 | create
6 | or replace function enable_rls_for_existing_users_on_journals (arg uuid[]) returns void as $$
7 | DECLARE
8 | u uuid;
9 | BEGIN
10 | FOREACH u IN ARRAY arg LOOP
11 | EXECUTE 'alter table public.journals_' || replace(u::text,'-','_') || ' enable row level security';
12 | EXECUTE 'drop policy if exists "User can manipulate only its own journal" on public.journals_' || replace(u::text,'-','_');
13 | EXECUTE 'create policy "User can manipulate only its own journal" on public.journals_' || replace(u::text,'-','_') || ' for all using (auth.uid () = user_id)';
14 | END LOOP;
15 | END;
16 | $$ language plpgsql;
17 |
18 | -- Run
19 | select
20 | enable_rls_for_existing_users_on_journals (array_agg(id))
21 | from
22 | auth.users;
23 |
24 | -- Update trigger function to create RLS for new users on journals
25 | create
26 | or replace function create_user_partition () returns trigger as $create_user_partition$
27 | BEGIN
28 | EXECUTE 'create table public.journals_' || replace(new.id::text,'-','_') || ' partition of public.journals for values in (''' || new.id || ''')';
29 | EXECUTE 'alter table public.journals_' || replace(new.id::text,'-','_') || ' enable row level security';
30 | EXECUTE 'create policy "User can manipulate only its own journal" on public.journals_' || replace(new.id::text,'-','_') || ' for all using (auth.uid () = user_id)';
31 | RETURN NEW;
32 | END;
33 | $create_user_partition$ language plpgsql security definer;
34 |
35 | -- Create RLS for existing users on entries_tags
36 | create
37 | or replace function enable_rls_for_existing_users_on_entries_tags (arg uuid[]) returns void as $$
38 | DECLARE
39 | u uuid;
40 | BEGIN
41 | FOREACH u IN ARRAY arg LOOP
42 | EXECUTE 'alter table public.entries_tags_' || replace(u::text,'-','_') || ' enable row level security';
43 | EXECUTE 'drop policy if exists "User can manipulate only its own tags in journals" on public.entries_tags_' || replace(u::text,'-','_');
44 | EXECUTE 'create policy "User can manipulate only its own tags in journals" on public.entries_tags_' || replace(u::text,'-','_') || ' for all using (auth.uid () = user_id)';
45 | END LOOP;
46 | END;
47 | $$ language plpgsql;
48 |
49 | -- Run
50 | select
51 | enable_rls_for_existing_users_on_entries_tags (array_agg(id))
52 | from
53 | auth.users;
54 |
55 | -- Update trigger function to create RLS for new users on entries_tags
56 | create
57 | or replace function create_user_partition_on_entries_tags () returns trigger as $create_user_partition_on_entries_tags$
58 | BEGIN
59 | EXECUTE 'create table public.entries_tags_' || replace(new.id::text,'-','_') || ' partition of public.entries_tags for values in (''' || new.id || ''')';
60 | EXECUTE 'alter table public.entries_tags_' || replace(new.id::text,'-','_') || ' enable row level security';
61 | EXECUTE 'create policy "User can manipulate only its own tags in journals" on public.entries_tags_' || replace(new.id::text,'-','_') || ' for all using (auth.uid () = user_id)';
62 | RETURN NEW;
63 | END;
64 | $create_user_partition_on_entries_tags$ language plpgsql security definer;
65 |
66 | --------------------------------------------
67 | -- Enable Realtime
68 | --------------------------------------------
69 | begin;
70 |
71 | drop publication if exists supabase_realtime;
72 |
73 | create publication supabase_realtime;
74 |
75 | commit;
76 |
77 | -- add journals to the publication
78 | alter publication supabase_realtime
79 | add table journals;
80 |
81 | -- add tags to the publication
82 | alter publication supabase_realtime
83 | add table tags;
84 |
85 | -- add entries_tags to the publication
86 | alter publication supabase_realtime
87 | add table entries_tags;
--------------------------------------------------------------------------------
/src/components/EntryTags/ListItemTagColorPicker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
2 | import { theme, lightTheme } from 'themes'
3 | import {
4 | useFloating,
5 | offset,
6 | FloatingTree,
7 | useListNavigation,
8 | useInteractions,
9 | useDismiss,
10 | FloatingFocusManager,
11 | useFocus,
12 | useFloatingNodeId,
13 | FloatingNode,
14 | FloatingPortal,
15 | } from '@floating-ui/react-dom-interactions'
16 | import {
17 | StyledEditTagColorPickerPopover,
18 | StyledEditTagColorPickerContainer,
19 | StyledColorPickerChevronIcon,
20 | StyledTagColorDot,
21 | StyledItem,
22 | StyledItemColorPicker,
23 | } from './styled'
24 | import type { Tag } from 'types'
25 | import { logger } from 'src/utils'
26 | import { useUserContext } from 'context'
27 |
28 | type ListItemTagColorPickerProps = {
29 | tag: Tag
30 | inputRef: React.MutableRefObject
31 | colorPickerOpen: boolean
32 | setColorPickerOpen: any
33 | tagEditColorRef: React.MutableRefObject
34 | }
35 |
36 | function ListItemTagColorPicker({
37 | tag,
38 | inputRef,
39 | colorPickerOpen,
40 | setColorPickerOpen,
41 | tagEditColorRef,
42 | }: ListItemTagColorPickerProps) {
43 | const [selectedColor, setSelectedColor] = useState(tagEditColorRef.current)
44 | const { session } = useUserContext()
45 | const { floating, strategy, reference, x, y, context } = useFloating({
46 | placement: 'left-start',
47 | open: colorPickerOpen,
48 | onOpenChange: setColorPickerOpen,
49 | middleware: [offset({ crossAxis: 0, mainAxis: 20 })],
50 | })
51 |
52 | const handleColorSelect = (e: any, color: Tag['color']) => {
53 | e.preventDefault()
54 | e.stopPropagation()
55 | tagEditColorRef.current = color
56 | setSelectedColor(color)
57 | logger('handleColorSelect')
58 | toggleOpen(e)
59 | window.electronAPI.capture({
60 | distinctId: session.user.id,
61 | event: 'tag edit color select',
62 | properties: { color },
63 | })
64 | }
65 |
66 | const toggleOpen = (e: any) => {
67 | e.stopPropagation()
68 | if (colorPickerOpen) {
69 | setColorPickerOpen(false)
70 | setTimeout(() => {
71 | inputRef.current.focus()
72 | }, 100)
73 | } else {
74 | setColorPickerOpen(true)
75 | }
76 | }
77 |
78 | return (
79 | <>
80 |
81 |
82 | toggleOpen(e)}
85 | />
86 |
87 |
88 | {colorPickerOpen && (
89 |
90 |
98 | {Object.keys(lightTheme.color.tags).map(
99 | (color: keyof typeof lightTheme.color.tags, i) => (
100 | handleColorSelect(e, color)}
103 | isActive={color == selectedColor}
104 | >
105 |
106 |
107 | )
108 | )}
109 |
110 |
111 | )}
112 |
113 | >
114 | )
115 | }
116 |
117 | export { ListItemTagColorPicker }
118 |
--------------------------------------------------------------------------------
/updates/darwin/x64/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "currentRelease": "1.0.5",
3 | "releases": [
4 | {
5 | "version": "1.0.5",
6 | "updateTo": {
7 | "version": "1.0.5",
8 | "pub_date": "2023-02-01T17:00:00+00:00",
9 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.5-x64.dmg",
10 | "name": "1.0.5",
11 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.5.zip"
12 | }
13 | },
14 | {
15 | "version": "1.0.4",
16 | "updateTo": {
17 | "version": "1.0.4",
18 | "pub_date": "2023-01-14T15:00:00+00:00",
19 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.4-x64.dmg",
20 | "name": "1.0.4",
21 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.4.zip"
22 | }
23 | },
24 | {
25 | "version": "1.0.3",
26 | "updateTo": {
27 | "version": "1.0.3",
28 | "pub_date": "2022-12-17T10:00:00+00:00",
29 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.3-x64.dmg",
30 | "name": "1.0.3",
31 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.3.zip"
32 | }
33 | },
34 | {
35 | "version": "1.0.2",
36 | "updateTo": {
37 | "version": "1.0.2",
38 | "pub_date": "2022-12-16T10:00:00+00:00",
39 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.2-x64.dmg",
40 | "name": "1.0.2",
41 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.2.zip"
42 | }
43 | },
44 | {
45 | "version": "1.0.1",
46 | "updateTo": {
47 | "version": "1.0.1",
48 | "pub_date": "2022-12-15T15:00:00+00:00",
49 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.1-x64.dmg",
50 | "name": "1.0.1",
51 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.1.zip"
52 | }
53 | },
54 | {
55 | "version": "1.0.0-beta.5",
56 | "updateTo": {
57 | "version": "1.0.0-beta.5",
58 | "pub_date": "2022-07-27T08:00:00+00:00",
59 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.0-beta.5-x64.dmg",
60 | "name": "1.0.0-beta.5",
61 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.0-beta.5.zip"
62 | }
63 | },
64 | {
65 | "version": "1.0.0-beta.4",
66 | "updateTo": {
67 | "version": "1.0.0-beta.4",
68 | "pub_date": "2022-07-20T10:30:00+00:00",
69 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.0-beta.4-x64.dmg",
70 | "name": "1.0.0-beta.4",
71 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.0-beta.4.zip"
72 | }
73 | },
74 | {
75 | "version": "1.0.0-beta.3",
76 | "updateTo": {
77 | "version": "1.0.0-beta.3",
78 | "pub_date": "2022-07-07T22:00:00+00:00",
79 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.0-beta.3-x64.dmg",
80 | "name": "1.0.0-beta.3",
81 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.0-beta.3.zip"
82 | }
83 | },
84 | {
85 | "version": "1.0.0-beta.2",
86 | "updateTo": {
87 | "version": "1.0.0-beta.2",
88 | "pub_date": "2022-07-01T20:00:00+00:00",
89 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.0-beta.2-x64.dmg",
90 | "name": "1.0.0-beta.2",
91 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.0-beta.2.zip"
92 | }
93 | },
94 | {
95 | "version": "1.0.0-beta.1",
96 | "updateTo": {
97 | "version": "1.0.0-beta.1",
98 | "pub_date": "2022-06-24T20:00:00+00:00",
99 | "notes": "https://desktop.journal.do/darwin/x64/Journal-1.0.0-beta.1-x64.dmg",
100 | "name": "1.0.0-beta.1",
101 | "url": "https://desktop.journal.do/darwin/x64/Journal-darwin-x64-1.0.0-beta.1.zip"
102 | }
103 | }
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/ConfirmationModal.tsx:
--------------------------------------------------------------------------------
1 | import * as Dialog from '@radix-ui/react-dialog'
2 | import React, { useRef } from 'react'
3 | import styled from 'styled-components'
4 | import { theme } from 'themes'
5 |
6 | const Overlay = styled(Dialog.Overlay)`
7 | position: fixed;
8 | top: 0;
9 | right: 0;
10 | bottom: 0;
11 | left: 0;
12 | background-color: ${theme('color.primary.surface', 0.8)};
13 | display: grid;
14 | place-items: center;
15 | `
16 |
17 | const Content = styled(Dialog.Content)`
18 | -webkit-app-region: no-drag;
19 | position: fixed;
20 | color: ${theme('color.primary.main')};
21 | background-color: ${theme('color.popper.surface')};
22 | border-radius: 12px;
23 | box-shadow: ${theme('style.shadow')};
24 | padding: 24px;
25 | display: flex;
26 | flex-direction: column;
27 | width: 330px;
28 | `
29 |
30 | const Trigger = styled(Dialog.Trigger)`
31 | border: 0;
32 | outline: 0;
33 | background: transparent;
34 | padding: 0;
35 | margin: 0;
36 | `
37 |
38 | const Title = styled(Dialog.Title)`
39 | font-style: normal;
40 | font-weight: 500;
41 | font-size: 18px;
42 | line-height: 28px;
43 | margin-bottom: 8px;
44 | letter-spacing: -0.03em;
45 | `
46 |
47 | const Description = styled.div`
48 | font-weight: 400;
49 | font-size: 12px;
50 | line-height: 15px;
51 | `
52 | const Actions = styled.div`
53 | display: flex;
54 | gap: 16px;
55 | margin-top: 32px;
56 | `
57 |
58 | const Close = styled.button`
59 | font-weight: 400;
60 | font-size: 14px;
61 | line-height: 17px;
62 | padding: 8px;
63 | color: ${theme('color.primary.main')};
64 | border: 0;
65 | border-radius: 6px;
66 | flex-grow: 1;
67 | background: transparent;
68 | cursor: pointer;
69 | outline: 0;
70 | &:hover {
71 | box-shadow: 0 0 0 3px ${theme('color.popper.border')};
72 | transition: box-shadow ${theme('animation.time.normal')} ease;
73 | }
74 | `
75 |
76 | const Action = styled.button`
77 | font-weight: 500;
78 | font-size: 14px;
79 | line-height: 17px;
80 | padding: 8px;
81 | color: ${theme('color.error.main')};
82 | border: 1px solid ${theme('color.error.main')};
83 | border-radius: 6px;
84 | flex-grow: 1;
85 | background: transparent;
86 | cursor: pointer;
87 | outline: 0;
88 | &:hover {
89 | box-shadow: 0 0 0 3px ${theme('color.error.main', 0.2)};
90 | transition: box-shadow ${theme('animation.time.normal')} ease;
91 | }
92 | `
93 |
94 | interface ConfirmationModalProps {
95 | action: any
96 | children: any
97 | titleText: string
98 | descriptionText?: string
99 | confirmActionText?: string
100 | cancelActionText?: string
101 | }
102 |
103 | const ConfirmationModal = ({
104 | action,
105 | children,
106 | titleText,
107 | descriptionText = '',
108 | confirmActionText = 'Confirm',
109 | cancelActionText = 'Cancel',
110 | }: any) => {
111 | const [open, setOpen] = React.useState(false)
112 | const closeRef = useRef(null)
113 |
114 | const performAction = (e: React.MouseEvent) => {
115 | e.preventDefault()
116 | e.stopPropagation()
117 | action()
118 | setOpen(false)
119 | }
120 |
121 | const close = (e: React.MouseEvent) => {
122 | e.preventDefault()
123 | e.stopPropagation()
124 | setOpen(false)
125 | }
126 |
127 | return (
128 |
129 | {children}
130 |
131 |
132 |
133 | {titleText}
134 | {descriptionText}
135 |
136 | performAction(e)}>{confirmActionText}
137 | close(e)}>
138 | {cancelActionText}
139 |
140 |
141 |
142 |
143 |
144 |
145 | )
146 | }
147 |
148 | export { ConfirmationModal }
149 |
--------------------------------------------------------------------------------
/updates/darwin/arm64/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "currentRelease": "1.0.5",
3 | "releases": [
4 | {
5 | "version": "1.0.5",
6 | "updateTo": {
7 | "version": "1.0.5",
8 | "pub_date": "2023-01-29T14:00:00+00:00",
9 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.5-arm64.dmg",
10 | "name": "1.0.5",
11 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.5.zip"
12 | }
13 | },
14 | {
15 | "version": "1.0.4",
16 | "updateTo": {
17 | "version": "1.0.4",
18 | "pub_date": "2023-01-14T15:00:00+00:00",
19 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.4-arm64.dmg",
20 | "name": "1.0.4",
21 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.4.zip"
22 | }
23 | },
24 | {
25 | "version": "1.0.3",
26 | "updateTo": {
27 | "version": "1.0.3",
28 | "pub_date": "2022-12-17T10:00:00+00:00",
29 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.3-arm64.dmg",
30 | "name": "1.0.3",
31 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.3.zip"
32 | }
33 | },
34 | {
35 | "version": "1.0.2",
36 | "updateTo": {
37 | "version": "1.0.2",
38 | "pub_date": "2022-12-16T10:00:00+00:00",
39 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.2-arm64.dmg",
40 | "name": "1.0.2",
41 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.2.zip"
42 | }
43 | },
44 | {
45 | "version": "1.0.1",
46 | "updateTo": {
47 | "version": "1.0.1",
48 | "pub_date": "2022-12-15T15:00:00+00:00",
49 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.1-arm64.dmg",
50 | "name": "1.0.1",
51 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.1.zip"
52 | }
53 | },
54 | {
55 | "version": "1.0.0-beta.5",
56 | "updateTo": {
57 | "version": "1.0.0-beta.5",
58 | "pub_date": "2022-07-27T08:00:00+00:00",
59 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.0-beta.5-arm64.dmg",
60 | "name": "1.0.0-beta.5",
61 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.0-beta.5.zip"
62 | }
63 | },
64 | {
65 | "version": "1.0.0-beta.4",
66 | "updateTo": {
67 | "version": "1.0.0-beta.4",
68 | "pub_date": "2022-07-20T10:30:00+00:00",
69 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.0-beta.4-arm64.dmg",
70 | "name": "1.0.0-beta.4",
71 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.0-beta.4.zip"
72 | }
73 | },
74 | {
75 | "version": "1.0.0-beta.3",
76 | "updateTo": {
77 | "version": "1.0.0-beta.3",
78 | "pub_date": "2022-07-07T22:00:00+00:00",
79 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.0-beta.3-arm64.dmg",
80 | "name": "1.0.0-beta.3",
81 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.0-beta.3.zip"
82 | }
83 | },
84 | {
85 | "version": "1.0.0-beta.2",
86 | "updateTo": {
87 | "version": "1.0.0-beta.2",
88 | "pub_date": "2022-07-01T20:00:00+00:00",
89 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.0-beta.2-arm64.dmg",
90 | "name": "1.0.0-beta.2",
91 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.0-beta.2.zip"
92 | }
93 | },
94 | {
95 | "version": "1.0.0-beta.1",
96 | "updateTo": {
97 | "version": "1.0.0-beta.1",
98 | "pub_date": "2022-06-24T20:00:00+00:00",
99 | "notes": "https://desktop.journal.do/darwin/arm64/Journal-1.0.0-beta.1-arm64.dmg",
100 | "name": "1.0.0-beta.1",
101 | "url": "https://desktop.journal.do/darwin/arm64/Journal-darwin-arm64-1.0.0-beta.1.zip"
102 | }
103 | }
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/tests/helpers/supabase.ts:
--------------------------------------------------------------------------------
1 | import { Client } from 'pg'
2 |
3 | const supabaseConn = {
4 | host: '127.0.0.1',
5 | port: 5432,
6 | user: 'postgres',
7 | password: 'your-super-secret-and-long-postgres-password',
8 | }
9 |
10 | const apikey =
11 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRuZmRhdW9vd3lycHhxb2RvbXFuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTQ1MzEzOTUsImV4cCI6MTk3MDEwNzM5NX0.XYkcWry-Eqm0-Hvq-arndEGhQn_yJvGF85-NNf9Sbvk'
12 |
13 | export async function supabaseLoginUser(email: string) {
14 | const data = await fetch(
15 | 'https://supabase.journal.local:8443/auth/v1/token?grant_type=password',
16 | {
17 | method: 'POST',
18 | body: JSON.stringify({
19 | email,
20 | password: '123456',
21 | }),
22 | headers: {
23 | apikey,
24 | 'Content-Type': 'application/json',
25 | },
26 | }
27 | ).then((r) => r.json())
28 | const { refresh_token } = data
29 | return refresh_token
30 | }
31 |
32 | export async function supabaseRegisterUser(email: string) {
33 | const data = await fetch('https://supabase.journal.local:8443/auth/v1/signup', {
34 | method: 'POST',
35 | body: JSON.stringify({
36 | email,
37 | password: '123456',
38 | }),
39 | headers: {
40 | apikey,
41 | 'Content-Type': 'application/json',
42 | },
43 | }).then((r) => r.json())
44 | return data
45 | }
46 |
47 | export async function supabaseDeleteUser(user_id: string) {
48 | const client = new Client(supabaseConn)
49 | await client.connect()
50 | await client.query(`delete from customers where id = '${user_id}'`)
51 | await client.query(`delete from entries_tags where user_id = '${user_id}'`)
52 | await client.query(`delete from journals where user_id = '${user_id}'`)
53 | await client.query(`delete from journals_catalog where user_id = '${user_id}'`)
54 | await client.query(`delete from subscriptions where user_id = '${user_id}'`)
55 | await client.query(`delete from tags where user_id = '${user_id}'`)
56 | await client.query(`delete from users where id = '${user_id}'`)
57 | await client.query(`delete from auth.users where id = '${user_id}'`)
58 | await client.query(`drop table entries_tags_${user_id.replaceAll('-', '_')}`)
59 | await client.query(`drop table journals_${user_id.replaceAll('-', '_')} cascade`)
60 | await client.end()
61 | }
62 |
63 | export async function supabaseGetEntry(user_id: string, day: string) {
64 | const client = new Client(supabaseConn)
65 | await client.connect()
66 | const entry = await client.query(
67 | `select * from journals where user_id = '${user_id}' and day = '${day}'`
68 | )
69 | await client.end()
70 | return entry.rows[0]
71 | }
72 |
73 | export async function supabaseCopyEntryToDayBefore(user_id: string, day: string) {
74 | const client = new Client(supabaseConn)
75 | await client.connect()
76 | await client.query(
77 | `insert into journals (user_id, day, created_at, modified_at, content, iv, revision) select user_id, day - interval '1' day, created_at, modified_at, content, iv, revision from journals where user_id ='${user_id}' and day = '${day}'`
78 | )
79 | await client.end()
80 | }
81 |
82 | export async function supabaseCopyEntryContent(user_id: string, dayFrom: string, dayTo: string) {
83 | const client = new Client(supabaseConn)
84 | await client.connect()
85 | const entryFrom = await client.query(
86 | `select * from journals where user_id = '${user_id}' and day = '${dayFrom}'`
87 | )
88 | await client.query(
89 | `UPDATE journals SET content = subquery.content, iv = subquery.iv, revision = revision + 1 FROM (SELECT content, iv FROM journals WHERE user_id = '${user_id}' and day = '${dayFrom}') AS subquery WHERE user_id = '${user_id}' and day = '${dayTo}'`
90 | )
91 |
92 | await client.end()
93 | }
94 |
95 | export async function supabaseDeleteEntry(user_id: string, day: string) {
96 | const client = new Client(supabaseConn)
97 | await client.connect()
98 | await client.query(`delete from journals where user_id ='${user_id}' and day = '${day}'`)
99 | await client.end()
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/ChangeCycle/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { theme } from 'themes'
3 | import { isDev, logger } from 'utils'
4 | import {
5 | useFloating,
6 | FloatingOverlay,
7 | useInteractions,
8 | useDismiss,
9 | useClick,
10 | FloatingFocusManager,
11 | useFloatingNodeId,
12 | FloatingNode,
13 | FloatingPortal,
14 | } from '@floating-ui/react-dom-interactions'
15 | import { useQuery } from '@tanstack/react-query'
16 | import { loadStripe } from '@stripe/stripe-js/pure'
17 | import { Elements } from '@stripe/react-stripe-js'
18 | import type { Countries, Price } from 'types'
19 | import { useUserContext } from 'context'
20 | import {
21 | getCustomer,
22 | fetchCountries,
23 | calcYearlyPlanSavings,
24 | } from '../../../../context/UserContext/subscriptions'
25 | import { CheckoutModalStyled } from './styled'
26 | import { Modal } from './Modal'
27 |
28 | interface SubscribeProps {
29 | renderTrigger: any
30 | prices: Price[]
31 | }
32 |
33 | ////////////////////////////
34 | // 🍔 ChangeCycle component
35 | ////////////////////////////
36 |
37 | const ChangeCycle = ({ renderTrigger, prices }: SubscribeProps) => {
38 | logger('ChangeCycle rerender')
39 | const [open, setOpen] = useState(false)
40 | const [stripePromise, setStripePromise] = useState(null)
41 | const nodeId = useFloatingNodeId()
42 | const { session } = useUserContext()
43 |
44 | useQuery({
45 | queryKey: ['stripePromise'],
46 | queryFn: async () => {
47 | const url = isDev() ? 'https://s.journal.local' : 'https://s.journal.do'
48 | const { publishableKey } = await fetch(`${url}/api/v1/config`).then((r) => r.json())
49 | setStripePromise(() => loadStripe(publishableKey))
50 | return publishableKey
51 | },
52 | })
53 |
54 | const { isLoading: billingInfoIsLoading, data: billingInfo } = useQuery({
55 | queryKey: ['billingInfo'],
56 | queryFn: async () => getCustomer(session.access_token),
57 | })
58 |
59 | const { reference, floating, context, refs } = useFloating({
60 | open,
61 | onOpenChange: setOpen,
62 | nodeId,
63 | })
64 |
65 | const { getReferenceProps, getFloatingProps } = useInteractions([
66 | useClick(context),
67 | useDismiss(context, {
68 | escapeKey: false,
69 | }),
70 | ])
71 |
72 | const handleCloseEsc = (e: any) => {
73 | if (e.key == 'Escape') {
74 | if (refs.floating.current && refs.floating.current.contains(document.activeElement)) {
75 | setOpen(false)
76 | }
77 | }
78 | }
79 |
80 | //////////////////////////
81 | // 🏓 useEffect
82 | //////////////////////////
83 |
84 | useEffect(() => {
85 | logger('✅ addEventListener')
86 | document.addEventListener('keydown', handleCloseEsc)
87 |
88 | return () => {
89 | logger('❌ removeEventListener')
90 | document.removeEventListener('keydown', handleCloseEsc)
91 | }
92 | }, [])
93 |
94 | useEffect(() => {
95 | if (open) {
96 | window.electronAPI.capture({
97 | distinctId: session.user.id,
98 | event: 'settings billing plan',
99 | properties: { action: 'upgrade-to-yearly' },
100 | })
101 | }
102 | }, [open])
103 |
104 | //////////////////////////
105 | // 🚀 Return
106 | //////////////////////////
107 |
108 | return (
109 |
110 | {renderTrigger({ open: () => setOpen(true), ref: reference, ...getReferenceProps() })}
111 |
112 | {open && (
113 |
122 |
123 |
124 |
125 |
131 |
132 |
133 |
134 |
135 | )}
136 |
137 |
138 | )
139 | }
140 |
141 | export { ChangeCycle }
142 |
--------------------------------------------------------------------------------
/src/components/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled from 'styled-components'
3 | import { theme } from 'themes'
4 | import { useUserContext } from 'context'
5 | import { Splash } from 'components'
6 | import { supabase, isDev, logger } from 'utils'
7 | import logo from '../../assets/icons/journaldo-logo@2x.png'
8 |
9 | const Container = styled.div`
10 | position: fixed;
11 | flex-direction: column;
12 | display: flex;
13 | gap: 8px;
14 | z-index: 9999;
15 | align-items: center;
16 | justify-content: center;
17 | width: 100%;
18 | height: 100%;
19 | -webkit-app-region: drag;
20 | `
21 |
22 | const Welcome = styled.h1`
23 | font-weight: 500;
24 | font-size: 28px;
25 | line-height: 34px;
26 | text-align: center;
27 | letter-spacing: -0.03em;
28 | color: ${theme('color.primary.main')};
29 | margin: 40px 0 32px 0;
30 | `
31 |
32 | const LoginButton = styled.a`
33 | font-weight: 600;
34 | font-size: 18px;
35 | line-height: 32px;
36 | text-align: center;
37 | letter-spacing: -0.03em;
38 | color: ${theme('color.primary.surface')};
39 | background-color: ${theme('color.primary.main')};
40 | padding: 16px 40px;
41 | border-radius: 100px;
42 | text-decoration: none;
43 | white-space: nowrap;
44 | outline: 0;
45 | `
46 |
47 | const ErrorMessage = styled.div`
48 | color: ${theme('color.error.main')};
49 | `
50 |
51 | const RTForm = styled.form`
52 | position: fixed;
53 | bottom: 16px;
54 | left: 16px;
55 | right: 16px;
56 | `
57 |
58 | const RTInput = styled.input`
59 | font-size: 16px;
60 | padding: 17px;
61 | display: block;
62 | margin: 4px;
63 | border-radius: 100px;
64 | border: 1px solid ${theme('color.primary.border')};
65 | background-color: ${theme('color.primary.surface')};
66 | color: ${theme('color.primary.main')};
67 | width: -webkit-fill-available;
68 | outline: 0;
69 | &:focus {
70 | border: 1px solid ${theme('color.secondary.hover')};
71 | }
72 | `
73 |
74 | const RTError = styled.div`
75 | font-size: 16px;
76 | padding: 17px;
77 | display: block;
78 | margin: 4px;
79 | color: ${theme('color.error.main')};
80 | `
81 |
82 | const Logo = styled.img`
83 | width: 64px;
84 | height: 64px;
85 | `
86 |
87 | const LoginWithToken = () => {
88 | const [authError, setAuthError] = useState('')
89 | const rt = useRef(null)
90 |
91 | const keyPress = async (e: React.KeyboardEvent) => {
92 | if (e.key === 'Enter') {
93 | let refreshToken = rt.current.value
94 | const { error } = await supabase.auth.signIn({ refreshToken })
95 | if (error) {
96 | logger(error)
97 | setAuthError(error.message)
98 | }
99 | } else {
100 | setAuthError('')
101 | }
102 | }
103 |
104 | const change = async (e: React.ChangeEvent) => {
105 | logger(e.target.value)
106 | try {
107 | const url = new URL(e.target.value)
108 | logger(url.host)
109 | const refreshToken = url.searchParams.get('refresh_token')
110 | const { error } = await supabase.auth.signIn({ refreshToken })
111 | if (error) {
112 | logger(error)
113 | setAuthError(error.message)
114 | }
115 | } catch {
116 | logger('no refresh_token found')
117 | }
118 | }
119 |
120 | return (
121 | e.preventDefault()}>
122 | {authError}
123 | keyPress(e)}
130 | onChange={(e) => change(e)}
131 | >
132 |
133 | )
134 | }
135 |
136 | const Login = () => {
137 | const { authError, session } = useUserContext()
138 |
139 | const log = () => {
140 | let lastUser = window.electronAPI.app.getKey('lastUser')
141 | window.electronAPI.capture({
142 | distinctId: lastUser,
143 | event: 'user login-with-browser',
144 | })
145 | }
146 |
147 | return (
148 | <>
149 |
150 |
151 |
152 | Welcome to Journal
153 | log()}>
154 | Log in with browser
155 |
156 | {authError}
157 | {isDev() && }
158 |
159 | >
160 | )
161 | }
162 |
163 | export { Login }
164 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/ImportExport/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { theme } from 'themes'
3 | import { logger } from 'utils'
4 | import { SectionTitleStyled } from '../styled'
5 | import { serialize } from 'remark-slate'
6 | import { useEntriesContext, useUserContext } from 'context'
7 | import type { Entry } from 'types'
8 | import { plateNodeTypes } from './nodeTypes'
9 | import styled from 'styled-components'
10 |
11 | const WrapperStyled = styled.div`
12 | display: flex;
13 | flex-direction: column;
14 | gap: 8px;
15 | `
16 | const ButtonsWrapperStyled = styled.div`
17 | display: flex;
18 | flex-wrap: wrap;
19 | gap: 16px;
20 | width: fit-content;
21 | padding-top: 16px;
22 | `
23 |
24 | const ButtonStyled = styled.button`
25 | font-weight: 500;
26 | font-size: 14px;
27 | line-height: 24px;
28 | white-space: nowrap;
29 | padding: 8px 16px;
30 | color: ${theme('color.popper.main')};
31 | border: 1px solid ${theme('color.popper.main')};
32 | border-radius: 6px;
33 | flex: 1;
34 | background: transparent;
35 | cursor: pointer;
36 | outline: 0;
37 | &:hover,
38 | &:focus {
39 | box-shadow: 0 0 0 3px ${theme('color.popper.main', 0.2)};
40 | transition: box-shadow ${theme('animation.time.normal')} ease;
41 | }
42 | &:disabled {
43 | opacity: 0.6;
44 | cursor: default;
45 | }
46 | `
47 |
48 | const TitleStyled = styled.div`
49 | font-style: normal;
50 | font-weight: 500;
51 | font-size: 16px;
52 | line-height: 24px;
53 | letter-spacing: -0.03em;
54 | `
55 |
56 | const TextStyled = styled.div`
57 | font-weight: 400;
58 | font-size: 14px;
59 | line-height: 20px;
60 | opacity: 0.6;
61 | `
62 |
63 | const toMarkdown = (content: any) => {
64 | return content
65 | .map((v: any) => {
66 | logger(v)
67 | if (v.type == 'hr') {
68 | return '---\n'
69 | }
70 | if (!!!v?.type) {
71 | logger('no type')
72 | v.type = 'p'
73 | }
74 | //@ts-ignore
75 | return serialize(v, { nodeTypes: plateNodeTypes })
76 | })
77 | .join('')
78 | }
79 |
80 | const exportTxt = (entries: Entry[]) => {
81 | try {
82 | const output = entries
83 | .map((entry) => {
84 | return 'Day: ' + entry.day + '\n\n' + toMarkdown(entry.content)
85 | })
86 | .join('\n\n\n')
87 | window.electronAPI.saveFile(output, 'txt')
88 | } catch (error) {
89 | logger('Error:')
90 | logger(error)
91 | }
92 | }
93 |
94 | const exportJson = (entries: Entry[]) => {
95 | try {
96 | const output = entries.map((entry) => {
97 | return {
98 | day: entry.day,
99 | content: toMarkdown(entry.content),
100 | }
101 | })
102 | window.electronAPI.saveFile(JSON.stringify(output), 'json')
103 | } catch (error) {
104 | logger('Error:')
105 | logger(error)
106 | }
107 | }
108 |
109 | const ImportExportTabContent = () => {
110 | logger('ImportExport re-render')
111 | const { userEntries } = useEntriesContext()
112 | const { session } = useUserContext()
113 |
114 | useEffect(() => {
115 | window.electronAPI.capture({
116 | distinctId: session.user.id,
117 | event: 'settings view-tab',
118 | properties: { tab: 'export' },
119 | })
120 | }, [])
121 |
122 | const exportTxtHandler = () => {
123 | exportTxt(userEntries.current)
124 | window.electronAPI.capture({
125 | distinctId: session.user.id,
126 | event: 'settings export-journal',
127 | properties: { format: 'txt' },
128 | })
129 | }
130 |
131 | const exportJsonHandler = () => {
132 | exportJson(userEntries.current)
133 | window.electronAPI.capture({
134 | distinctId: session.user.id,
135 | event: 'settings export-journal',
136 | properties: { format: 'json' },
137 | })
138 | }
139 |
140 | return (
141 | <>
142 | Export
143 |
144 | Export all Journal entries as markdown:
145 |
146 | You own your data and you can download it any time.
147 |
148 | All entries are automatically synced with the cloud, so you don't have to make backups.
149 |
150 |
151 | exportTxtHandler()}>Journal.txt ↓
152 | exportJsonHandler()}>Journal.json ↓
153 |
154 |
155 | >
156 | )
157 | }
158 |
159 | export { ImportExportTabContent }
160 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Subscribe/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { theme } from 'themes'
3 | import { isDev, logger } from 'utils'
4 | import {
5 | useFloating,
6 | FloatingOverlay,
7 | useInteractions,
8 | useDismiss,
9 | useClick,
10 | FloatingFocusManager,
11 | useFloatingNodeId,
12 | FloatingNode,
13 | FloatingPortal,
14 | } from '@floating-ui/react-dom-interactions'
15 | import { useQuery } from '@tanstack/react-query'
16 | import { loadStripe } from '@stripe/stripe-js/pure'
17 | import { Elements } from '@stripe/react-stripe-js'
18 | import type { Countries, Price } from 'types'
19 | import { useUserContext } from 'context'
20 | import { getCustomer, fetchCountries } from '../../../../context/UserContext/subscriptions'
21 | import { CheckoutModalStyled } from './styled'
22 | import { Modal } from './Modal'
23 |
24 | interface SubscribeProps {
25 | renderTrigger: any
26 | prices: Price[]
27 | billingInterval: 'year' | 'month'
28 | }
29 |
30 | //////////////////////////
31 | // 🍔 Subscribe component
32 | //////////////////////////
33 |
34 | const Subscribe = ({ renderTrigger, prices, billingInterval }: SubscribeProps) => {
35 | logger('Subscribe rerender')
36 | const [stripePromise, setStripePromise] = useState(null)
37 | const [open, setOpen] = useState(false)
38 | const nodeId = useFloatingNodeId()
39 | const { session } = useUserContext()
40 |
41 | const { data: countries } = useQuery({
42 | queryKey: ['countries'],
43 | queryFn: fetchCountries,
44 | })
45 |
46 | const { isLoading: billingInfoIsLoading, data: billingInfo } = useQuery({
47 | queryKey: ['billingInfo'],
48 | queryFn: async () => getCustomer(session.access_token),
49 | })
50 |
51 | useQuery({
52 | queryKey: ['stripePromise'],
53 | queryFn: async () => {
54 | const url = isDev() ? 'https://s.journal.local' : 'https://s.journal.do'
55 | const { publishableKey } = await fetch(`${url}/api/v1/config`).then((r) => r.json())
56 | setStripePromise(() => loadStripe(publishableKey))
57 | return publishableKey
58 | },
59 | })
60 |
61 | const { reference, floating, context, refs } = useFloating({
62 | open,
63 | onOpenChange: setOpen,
64 | nodeId,
65 | })
66 |
67 | const { getReferenceProps, getFloatingProps } = useInteractions([
68 | useClick(context),
69 | useDismiss(context, {
70 | escapeKey: false,
71 | }),
72 | ])
73 |
74 | const handleCloseEsc = (e: any) => {
75 | if (e.key == 'Escape') {
76 | if (refs.floating.current && refs.floating.current.contains(document.activeElement)) {
77 | setOpen(false)
78 | }
79 | }
80 | }
81 |
82 | //////////////////////////
83 | // 🏓 useEffect
84 | //////////////////////////
85 |
86 | useEffect(() => {
87 | logger('✅ addEventListener')
88 | document.addEventListener('keydown', handleCloseEsc)
89 |
90 | return () => {
91 | logger('❌ removeEventListener')
92 | document.removeEventListener('keydown', handleCloseEsc)
93 | }
94 | }, [])
95 |
96 | useEffect(() => {
97 | if (open) {
98 | window.electronAPI.capture({
99 | distinctId: session.user.id,
100 | event: 'settings upgrade upgrade-cta',
101 | properties: { plan: 'Writer' },
102 | })
103 | }
104 | }, [open])
105 |
106 | //////////////////////////
107 | // 🚀 Return
108 | //////////////////////////
109 |
110 | return (
111 |
112 | {renderTrigger({ open: () => setOpen(true), ref: reference, ...getReferenceProps() })}
113 |
114 | {open && (
115 |
124 |
125 |
126 |
127 |
135 |
136 |
137 |
138 |
139 | )}
140 |
141 |
142 | )
143 | }
144 |
145 | export { Subscribe }
146 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/AddCard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { theme, getCSSVar } from 'themes'
3 | import { isDev, logger } from 'utils'
4 | import {
5 | useFloating,
6 | FloatingOverlay,
7 | useInteractions,
8 | useDismiss,
9 | useClick,
10 | FloatingFocusManager,
11 | useFloatingNodeId,
12 | FloatingNode,
13 | FloatingPortal,
14 | } from '@floating-ui/react-dom-interactions'
15 | import { loadStripe } from '@stripe/stripe-js/pure'
16 | import { useQuery } from '@tanstack/react-query'
17 | import { CheckoutModalStyled } from './styled'
18 | import { Modal } from './Modal'
19 | import { Elements } from '@stripe/react-stripe-js'
20 | import { useUserContext } from 'context'
21 | import {
22 | getSubscription,
23 | getCustomer,
24 | fetchCountries,
25 | createSetupIntent,
26 | } from '../../../../context/UserContext/subscriptions'
27 |
28 | interface AddCardProps {
29 | renderTrigger: any
30 | isUpdate?: boolean
31 | }
32 |
33 | //////////////////////////
34 | // 🍔 AddCard component
35 | //////////////////////////
36 |
37 | const AddCard = ({ renderTrigger, isUpdate = false }: AddCardProps) => {
38 | logger('AddCard rerender')
39 | const { session, subscription, createSubscription } = useUserContext()
40 | const [stripePromise, setStripePromise] = useState(null)
41 | const [open, setOpen] = useState(false)
42 | const nodeId = useFloatingNodeId()
43 |
44 | const { data: countries } = useQuery({
45 | queryKey: ['countries'],
46 | queryFn: fetchCountries,
47 | })
48 |
49 | const {
50 | isLoading: billingInfoIsLoading,
51 | data: billingInfo,
52 | refetch: refetchBillingInfo,
53 | } = useQuery({
54 | queryKey: ['billingInfo'],
55 | queryFn: async () => getCustomer(session.access_token),
56 | })
57 |
58 | useQuery({
59 | queryKey: ['stripePromise'],
60 | queryFn: async () => {
61 | const url = isDev() ? 'https://s.journal.local' : 'https://s.journal.do'
62 | const { publishableKey } = await fetch(`${url}/api/v1/config`).then((r) => r.json())
63 | setStripePromise(() => loadStripe(publishableKey))
64 | return publishableKey
65 | },
66 | })
67 |
68 | const { reference, floating, context, refs } = useFloating({
69 | open,
70 | onOpenChange: setOpen,
71 | nodeId,
72 | })
73 |
74 | const { getReferenceProps, getFloatingProps } = useInteractions([
75 | useClick(context),
76 | useDismiss(context, {
77 | escapeKey: false,
78 | }),
79 | ])
80 |
81 | const handleCloseEsc = (e: any) => {
82 | if (e.key == 'Escape') {
83 | if (refs.floating.current && refs.floating.current.contains(document.activeElement)) {
84 | setOpen(false)
85 | }
86 | }
87 | }
88 |
89 | //////////////////////////
90 | // 🏓 useEffect
91 | //////////////////////////
92 |
93 | useEffect(() => {
94 | logger('✅ addEventListener')
95 | document.addEventListener('keydown', handleCloseEsc)
96 |
97 | return () => {
98 | logger('❌ removeEventListener')
99 | document.removeEventListener('keydown', handleCloseEsc)
100 | }
101 | }, [])
102 |
103 | useEffect(() => {
104 | if (open) {
105 | window.electronAPI.capture({
106 | distinctId: session.user.id,
107 | event: 'settings billing payment-method',
108 | properties: { action: isUpdate ? 'update' : 'add' },
109 | })
110 | }
111 | }, [open])
112 |
113 | //////////////////////////
114 | // 🚀 Return
115 | //////////////////////////
116 |
117 | return (
118 |
119 | {renderTrigger({ open: () => setOpen(true), ref: reference, ...getReferenceProps() })}
120 |
121 | {open && (
122 |
131 |
132 |
133 |
134 |
142 |
143 |
144 |
145 |
146 | )}
147 |
148 |
149 | )
150 | }
151 |
152 | export { AddCard }
153 |
--------------------------------------------------------------------------------
/src/utils/misc.ts:
--------------------------------------------------------------------------------
1 | import type { PostgrestError } from '@supabase/supabase-js'
2 | import { getNodeString } from '@udecode/plate'
3 |
4 | function isRenderer() {
5 | // running in a web browser
6 | if (typeof process === 'undefined') return true
7 |
8 | // node-integration is disabled
9 | if (!process) return true
10 |
11 | // We're in node.js somehow
12 | if (!process.type) return false
13 |
14 | return process.type === 'renderer'
15 | }
16 |
17 | function shallowEqual(object1: any, object2: any) {
18 | const keys1 = Object.keys(object1)
19 | const keys2 = Object.keys(object2)
20 | if (keys1.length !== keys2.length) {
21 | return false
22 | }
23 | for (let key of keys1) {
24 | if (object1[key] !== object2[key]) {
25 | return false
26 | }
27 | }
28 | return true
29 | }
30 |
31 | function arrayEquals(a: Array, b: Array) {
32 | return (
33 | Array.isArray(a) &&
34 | Array.isArray(b) &&
35 | a.length === b.length &&
36 | a.every((val, index) => val === b[index])
37 | )
38 | }
39 |
40 | function isArrayEmpty(a: any[]) {
41 | return Array.isArray(a) && !a.length
42 | }
43 |
44 | const countWords = (text: any) => {
45 | let res = []
46 | let str = text.replace(/[\t\n\r\.\?\!]/gm, ' ').split(' ')
47 | str.map((s: any) => {
48 | let trimStr = s.trim()
49 | if (trimStr.length > 0) {
50 | res.push(trimStr)
51 | }
52 | })
53 | return res.length
54 | }
55 |
56 | const countEntryWords = (content: object[]) => {
57 | if (Array.isArray(content)) {
58 | return countWords(content.map((n: any) => getNodeString(n)).join(' '))
59 | } else {
60 | return 0
61 | }
62 | }
63 |
64 | const entryHasNoContent = (content: any[]) => {
65 | return content.some((n: any) => !getNodeString(n))
66 | }
67 |
68 | const setCssVars = (items: any, prefix = '-'): void => {
69 | Object.entries(items).flatMap(([key, value]: any) => {
70 | const varName = `${prefix}-${key}`
71 | if (typeof value === 'object') {
72 | setCssVars(value, varName)
73 | } else {
74 | document.documentElement.style.setProperty(varName, value)
75 | }
76 | })
77 | }
78 |
79 | const createCssVar = (items: [string], prefix = '-'): string[] =>
80 | Object.entries(items).flatMap(([key, value]: any) => {
81 | const varName = `${prefix}-${key}`
82 | if (typeof value === 'object') return createCssVar(value, varName)
83 | return `${varName}:${value}`
84 | })
85 |
86 | const createCssVars = (themeColors: any) => createCssVar(themeColors).join(';')
87 |
88 | const alphaToHex = (alpha: number) => {
89 | if (alpha < 0) alpha = 0
90 | if (alpha > 100) alpha = 100
91 | let multiplier = 255 / 100
92 | let val = Math.round(alpha * multiplier).toString(16)
93 | return val.length == 1 ? '0' + val : val
94 | }
95 |
96 | function ordinal(n: number) {
97 | var s = ['th', 'st', 'nd', 'rd']
98 | var v = n % 100
99 | return n + (s[(v - 20) % 10] || s[v] || s[0])
100 | }
101 |
102 | function isDev() {
103 | return process.env.NODE_ENV == 'development' || isTesting()
104 | }
105 |
106 | function isTesting() {
107 | if (isRenderer()) {
108 | return window.electronAPI.isTesting()
109 | } else {
110 | return process.argv.includes('testing')
111 | }
112 | }
113 |
114 | function isUnauthorized(error: PostgrestError) {
115 | if (error.message == 'JWT expired' || error.code == '42501') {
116 | return true
117 | } else {
118 | return false
119 | }
120 | }
121 |
122 | function isUniqueViolation(error: PostgrestError) {
123 | if (error.code == '23505') {
124 | return true
125 | } else {
126 | return false
127 | }
128 | }
129 |
130 | function isForeignKeyViolation(error: PostgrestError) {
131 | if (error.code == '23503') {
132 | return true
133 | } else {
134 | return false
135 | }
136 | }
137 |
138 | function randomInt(max: number) {
139 | // max number (exclusive)
140 | // e.g. max=3 -> 0,1,2
141 | return (max * Math.random()) << 0
142 | }
143 |
144 | const awaitTimeout = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay))
145 |
146 | const capitalize = (word: string) => {
147 | return word[0].toUpperCase() + word.substring(1)
148 | }
149 |
150 | const displayAmount = (amount: number) => {
151 | // prevent amount being -0
152 | if (amount == 0) {
153 | amount = 0
154 | }
155 | const formatter = new Intl.NumberFormat('en-US', {
156 | style: 'currency',
157 | currency: 'USD',
158 | })
159 | const a = formatter.format(amount / 100)
160 | if (a.slice(-3) == '.00') {
161 | return a.slice(0, -3)
162 | } else {
163 | return a
164 | }
165 | }
166 |
167 | export {
168 | shallowEqual,
169 | arrayEquals,
170 | countWords,
171 | createCssVars,
172 | setCssVars,
173 | alphaToHex,
174 | ordinal,
175 | isDev,
176 | isTesting,
177 | isUnauthorized,
178 | isUniqueViolation,
179 | isForeignKeyViolation,
180 | randomInt,
181 | isArrayEmpty,
182 | entryHasNoContent,
183 | countEntryWords,
184 | awaitTimeout,
185 | capitalize,
186 | displayAmount,
187 | }
188 |
--------------------------------------------------------------------------------
/src/components/Menu/Settings/Upgrade/Features.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import { theme } from 'themes'
4 | import { Icon } from 'components'
5 | import * as Accordion from '@radix-ui/react-accordion'
6 | import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
7 | import { nanoid } from 'nanoid'
8 | import { logger, supabase, awaitTimeout } from 'utils'
9 | import Skeleton from 'react-loading-skeleton'
10 | import { useQuery } from '@tanstack/react-query'
11 | import { useUserContext } from 'context'
12 | import { onlyText } from 'react-children-utilities'
13 |
14 | const Chevron = styled(Icon)`
15 | transition: transform ${theme('animation.time.normal')};
16 | [data-state='open'] & {
17 | transform: rotate(180deg);
18 | }
19 | `
20 |
21 | const AccordionItem = styled(Accordion.Item)``
22 |
23 | const Open = keyframes`
24 | 0% {
25 | height: 0;
26 | opacity: 0;
27 | padding-bottom: 8px;
28 | }
29 | 100% {
30 | height: var(--radix-accordion-content-height);
31 | opacity: 0.8;
32 | padding-bottom: 16px;
33 | }
34 | `
35 |
36 | const Close = keyframes`
37 | 0% {
38 | height: var(--radix-accordion-content-height);
39 | opacity: 0.8;
40 | padding-bottom: 16px;
41 | }
42 | 100% {
43 | height: 0;
44 | opacity: 0;
45 | padding-bottom: 8px;
46 | }
47 | `
48 |
49 | const AccordionContent = styled(Accordion.Content)`
50 | font-weight: 400;
51 | font-size: 12px;
52 | line-height: 20px;
53 | opacity: 0.8;
54 | letter-spacing: normal;
55 | overflow: hidden;
56 | &[data-state='open'] {
57 | animation: ${Open} ${theme('animation.time.normal')} ease-out;
58 | animation-fill-mode: both;
59 | }
60 | &[data-state='closed'] {
61 | animation: ${Close} ${theme('animation.time.normal')} ease-out;
62 | animation-fill-mode: both;
63 | }
64 | `
65 |
66 | const AccordionHeader = styled(Accordion.Header)`
67 | margin: 0;
68 | `
69 |
70 | const AccordionTrigger = styled(Accordion.Trigger)`
71 | display: flex;
72 | color: ${theme('color.popper.main')};
73 | cursor: pointer;
74 | width: -webkit-fill-available;
75 | text-align: left;
76 | background-color: transparent;
77 | border: 0;
78 | border-top: 1px solid ${theme('color.popper.border')};
79 | outline: 0;
80 | padding: 8px 0;
81 | font-weight: 500;
82 | font-size: 12px;
83 | line-height: 20px;
84 | `
85 |
86 | const TriggerLabel = styled.span`
87 | flex-grow: 1;
88 | `
89 |
90 | const H2 = styled.div`
91 | font-weight: 500;
92 | font-size: 16px;
93 | line-height: 24px;
94 | letter-spacing: -0.03em;
95 | color: ${theme('color.popper.main')};
96 | margin-bottom: 16px;
97 | margin-top: 25px;
98 | `
99 |
100 | const SkeletonContainer = styled.div`
101 | display: block;
102 | & span {
103 | margin: 4px 0;
104 | }
105 | `
106 |
107 | const fetchFeaturesAndFAQ = async () => {
108 | const { data, error } = await supabase
109 | .from('website_pages')
110 | .select('content')
111 | .eq('page', 'pricing')
112 | .single()
113 | if (error) {
114 | throw new Error(error.message)
115 | }
116 | // await awaitTimeout(2000)
117 | return await window.electronAPI.mdxSerialize(data?.content ?? '')
118 | }
119 |
120 | const Features = () => {
121 | logger('Features rerender')
122 | const { session } = useUserContext()
123 | const { isLoading, isError, data, error } = useQuery({
124 | queryKey: ['features'],
125 | queryFn: fetchFeaturesAndFAQ,
126 | })
127 |
128 | const captureAccordionClick = (item: string) => {
129 | window.electronAPI.capture({
130 | distinctId: session.user.id,
131 | event: 'settings upgrade feature-expand',
132 | properties: { item },
133 | })
134 | }
135 |
136 | const components = {
137 | h2: (props: any) => ,
138 | Acc: (props: any) => ,
139 | AccItem: (props: any) => ,
140 | AccTitle: ({ children, ...rest }: any) => (
141 | <>
142 | captureAccordionClick(onlyText(children))} {...rest}>
143 |
144 | {children}
145 |
146 |
147 |
148 | >
149 | ),
150 | AccContent: (props: any) => ,
151 | }
152 |
153 | if (isLoading) {
154 | return (
155 |
156 |
163 |
164 | )
165 | }
166 |
167 | if (isError) {
168 | return <>>
169 | }
170 |
171 | return
172 | }
173 |
174 | export { Features }
175 |
--------------------------------------------------------------------------------
/src/components/Menu/AppearanceToolbar/styled.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import * as Toolbar from '@radix-ui/react-toolbar'
3 | import styled, { keyframes } from 'styled-components'
4 | import { theme } from 'themes'
5 | import { breakpoints } from 'utils'
6 |
7 | const reveal = keyframes`
8 | 0% {
9 | margin-bottom: -24px;
10 | opacity: 0;
11 | }
12 | 100% {
13 | margin-bottom: 0px;
14 | opacity: 1;
15 | }
16 | `
17 |
18 | interface ToggleButtonProps {
19 | padding?: string
20 | fontName?: string
21 | }
22 |
23 | const ToggleButtonStyled = styled(({ padding, fontName, ...props }) => (
24 |
25 | ))`
26 | height: 48px;
27 | font-size: 14px;
28 | line-height: 14px;
29 | min-width: 48px;
30 | padding: ${(props) => (props.padding ? props.padding : '16px')};
31 | font-family: ${(props) => (props.fontName ? props.fontName : 'inherit')};
32 | border-radius: 100px;
33 | cursor: pointer;
34 | border: 1px solid ${theme('color.popper.border')};
35 | background-color: ${theme('color.popper.surface')};
36 | color: ${theme('color.popper.disabled')};
37 | transition: opacity ${theme('animation.time.normal')};
38 | &:disabled {
39 | cursor: initial;
40 | }
41 | &:focus {
42 | outline: 0;
43 | }
44 | &:hover {
45 | transition: border ${theme('animation.time.normal')};
46 | border: 1px solid ${theme('color.popper.disabled')};
47 | }
48 | &[data-state='on'] {
49 | opacity: 1;
50 | border: 1px solid ${theme('color.popper.main')};
51 | border: 1px solid ${theme('color.popper.main')};
52 | color: ${theme('color.popper.main')};
53 | }
54 | `
55 |
56 | const ToggleButtonSmallStyled = styled(({ padding, fontName, ...props }) => (
57 |
58 | ))`
59 | height: 22px;
60 | font-size: 14px;
61 | line-height: 14px;
62 | min-width: 22px;
63 | display: flex;
64 | padding: 0;
65 | align-items: center;
66 | justify-content: center;
67 | font-family: ${(props) => (props.fontName ? props.fontName : 'inherit')};
68 | border-radius: 100px;
69 | cursor: pointer;
70 | border: 1px solid ${theme('color.popper.border')};
71 | background-color: ${theme('color.popper.surface')};
72 | color: ${theme('color.popper.disabled')};
73 | transition: ${theme('animation.time.normal')};
74 | &:disabled {
75 | cursor: initial;
76 | }
77 | &:focus {
78 | outline: 0;
79 | }
80 | &:hover {
81 | border: 1px solid ${theme('color.popper.disabled')};
82 | }
83 | &[data-state='on'] {
84 | opacity: 1;
85 | border: 1px solid ${theme('color.popper.main')};
86 | border: 1px solid ${theme('color.popper.main')};
87 | color: ${theme('color.popper.main')};
88 | }
89 | `
90 |
91 | const AppearanceToolbarWrapperStyled = styled.div`
92 | position: fixed;
93 | bottom: 80px;
94 | left: 0;
95 | right: 0;
96 | display: flex;
97 | justify-content: center;
98 | @media ${breakpoints.s} {
99 | transform: scale(0.9);
100 | bottom: 60px;
101 | }
102 | @media ${breakpoints.xs} {
103 | transform: scale(0.7);
104 | bottom: 40px;
105 | }
106 | `
107 |
108 | const AppearanceToolbarStyled = styled(Toolbar.Root)`
109 | padding: 12px;
110 | border-radius: 100px;
111 | gap: 8px;
112 | display: flex;
113 | box-shadow: ${theme('style.shadow')};
114 | background-color: ${theme('color.popper.surface')};
115 | transition: 0;
116 | animation-name: ${reveal};
117 | animation-duration: ${theme('animation.time.normal')};
118 | animation-timing-function: ${theme('animation.timingFunction.dynamic')};
119 | animation-fill-mode: both;
120 | -webkit-app-region: no-drag;
121 | `
122 | const ToggleGroupStyled = styled(Toolbar.ToggleGroup)`
123 | gap: 8px;
124 | display: flex;
125 | `
126 |
127 | const ToggleGroupNestedStyled = styled.div`
128 | width: 22px;
129 | gap: 4px;
130 | display: flex;
131 | flex-direction: column;
132 | `
133 |
134 | const ToggleFontAStyled = styled(ToggleButtonStyled)`
135 | font-size: 13px;
136 | `
137 |
138 | const ToggleFontAAStyled = styled(ToggleButtonStyled)`
139 | font-size: 16px;
140 | `
141 |
142 | const ToggleFontAAAStyled = styled(ToggleButtonStyled)`
143 | font-size: 22px;
144 | `
145 | interface ColorSwatchProps {
146 | fillColor: string
147 | }
148 |
149 | const ColorSwatchStyled = styled.div`
150 | height: 31px;
151 | width: 31px;
152 | border-radius: 100px;
153 | background-color: ${(props) => props.fillColor};
154 | `
155 |
156 | const ColorSwatchSmallStyled = styled.div`
157 | height: 14px;
158 | width: 14px;
159 | border-radius: 100px;
160 | background-color: ${(props) => props.fillColor};
161 | `
162 |
163 | const HorizontalDividerStyled = styled(Toolbar.Separator)`
164 | background-color: ${theme('color.popper.border')};
165 | width: 1px;
166 | margin: 4px 8px;
167 | `
168 |
169 | export {
170 | AppearanceToolbarWrapperStyled,
171 | AppearanceToolbarStyled,
172 | ToggleButtonStyled,
173 | ToggleButtonSmallStyled,
174 | ToggleGroupStyled,
175 | ToggleGroupNestedStyled,
176 | ToggleFontAStyled,
177 | ToggleFontAAStyled,
178 | ToggleFontAAAStyled,
179 | ColorSwatchStyled,
180 | ColorSwatchSmallStyled,
181 | HorizontalDividerStyled,
182 | }
183 |
--------------------------------------------------------------------------------