├── README.md ├── .gitignore ├── assets ├── logo.png ├── logo2.png ├── logo-transparent.png ├── logo-with-border.png ├── logo-with-shadow.png ├── particlesjs-config.json └── global.css ├── src ├── pages │ ├── Landing │ │ ├── assets │ │ │ ├── img.png │ │ │ ├── avatar.png │ │ │ ├── cover.png │ │ │ ├── logo2.png │ │ │ ├── benefit-0.png │ │ │ ├── benefit-1.png │ │ │ ├── benefit-2.png │ │ │ ├── benefit-3.png │ │ │ ├── features_sp.png │ │ │ ├── full-bg-a.png │ │ │ ├── full-bg-b.jpg │ │ │ ├── header_bg.png │ │ │ ├── promise-0-0.png │ │ │ ├── promise-0-1.png │ │ │ ├── promise-1-0.png │ │ │ ├── promise-1-1.png │ │ │ ├── promise-2-0.png │ │ │ ├── promise-2-1.png │ │ │ ├── tutorials-1.png │ │ │ ├── full-bg-text.png │ │ │ ├── icon_journals.png │ │ │ ├── gradient-bg-demo.png │ │ │ ├── icon_daily_plan.png │ │ │ ├── icon_relations.png │ │ │ ├── light_spots_bg.png │ │ │ ├── rtc-abstraction.png │ │ │ ├── whiteboard-demo.png │ │ │ ├── full-bg-c-gradient.png │ │ │ ├── icon_data_control.png │ │ │ ├── product_hunt_logo.png │ │ │ ├── filesync-abstraction.png │ │ │ ├── footer-gradients-bg.png │ │ │ ├── full-bg-text_mobile.png │ │ │ ├── light_spots_full_bg.png │ │ │ ├── light_spots_full_bg2.png │ │ │ ├── light_spots_top_arrow.png │ │ │ ├── sync-indicator-demo.png │ │ │ ├── whiteboard-demo-small.png │ │ │ ├── app-window-placeholder-bg.png │ │ │ ├── whiteboard_canvas_fadeout.png │ │ │ ├── app-window-placeholder-bg-no-logo.png │ │ │ └── logo-dots.svg │ │ ├── index.ts │ │ ├── common.tsx │ │ ├── HeadShowcase.tsx │ │ ├── DailyShowcase.tsx │ │ ├── LandingFooterNav.tsx │ │ ├── TutorialTips.tsx │ │ └── TutorialShowcase.tsx │ ├── Pro │ │ ├── assets │ │ │ ├── page_bg1.png │ │ │ ├── pro_card_bg1.png │ │ │ └── pro_card_bg2.png │ │ └── modals.tsx │ ├── Downloads │ │ ├── assets │ │ │ ├── p_ios.png │ │ │ ├── icon_m1.png │ │ │ ├── p_android.png │ │ │ ├── p_linux.png │ │ │ ├── p_macos.png │ │ │ ├── p_windows.png │ │ │ ├── dl_head_bg.jpg │ │ │ ├── icon_apple.png │ │ │ ├── icon_intel.png │ │ │ ├── icon_linux.png │ │ │ ├── ios_app_qr.png │ │ │ ├── dl_head_bg_2.png │ │ │ ├── icon_google_play.png │ │ │ └── icon_windows_64.png │ │ ├── index.css │ │ └── index.tsx │ ├── User │ │ ├── Terms.tsx │ │ ├── PrivacyPolicy.tsx │ │ ├── index.tsx │ │ ├── Login.tsx │ │ ├── amplify.ts │ │ └── index.css │ └── Home.tsx ├── hooks.ts ├── main.tsx ├── components │ ├── Cards.tsx │ ├── Modal.tsx │ ├── Animations.tsx │ ├── Buttons.tsx │ ├── PrivacyBanner.tsx │ ├── Icons.tsx │ ├── Dropdown.tsx │ ├── utils.ts │ └── Headbar.tsx ├── types.ts ├── App.tsx └── state.ts ├── .postcssrc ├── .parcelrc ├── wrangler.toml ├── public └── auth_callback.html ├── .markedrc ├── LICENSE ├── .github └── workflows │ └── deploy-s3.yml ├── package.json ├── index.ts ├── index.html ├── tailwind.config.js └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # Logseq home page 2 | https://logseq.com 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache 2 | node_modules 3 | dist 4 | .DS_Store 5 | .idea -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/assets/logo.png -------------------------------------------------------------------------------- /assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/assets/logo2.png -------------------------------------------------------------------------------- /assets/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/assets/logo-transparent.png -------------------------------------------------------------------------------- /assets/logo-with-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/assets/logo-with-border.png -------------------------------------------------------------------------------- /assets/logo-with-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/assets/logo-with-shadow.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/img.png -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {}, 4 | "tailwindcss/nesting": "postcss-nesting" 5 | } 6 | } -------------------------------------------------------------------------------- /src/pages/Landing/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/avatar.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/cover.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/logo2.png -------------------------------------------------------------------------------- /src/pages/Pro/assets/page_bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Pro/assets/page_bg1.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/p_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/p_ios.png -------------------------------------------------------------------------------- /src/pages/Pro/assets/pro_card_bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Pro/assets/pro_card_bg1.png -------------------------------------------------------------------------------- /src/pages/Pro/assets/pro_card_bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Pro/assets/pro_card_bg2.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/icon_m1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/icon_m1.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/p_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/p_android.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/p_linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/p_linux.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/p_macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/p_macos.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/p_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/p_windows.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/benefit-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/benefit-0.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/benefit-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/benefit-1.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/benefit-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/benefit-2.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/benefit-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/benefit-3.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/features_sp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/features_sp.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/full-bg-a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/full-bg-a.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/full-bg-b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/full-bg-b.jpg -------------------------------------------------------------------------------- /src/pages/Landing/assets/header_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/header_bg.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/promise-0-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/promise-0-0.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/promise-0-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/promise-0-1.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/promise-1-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/promise-1-0.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/promise-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/promise-1-1.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/promise-2-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/promise-2-0.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/promise-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/promise-2-1.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/tutorials-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/tutorials-1.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/dl_head_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/dl_head_bg.jpg -------------------------------------------------------------------------------- /src/pages/Downloads/assets/icon_apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/icon_apple.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/icon_intel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/icon_intel.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/icon_linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/icon_linux.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/ios_app_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/ios_app_qr.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/full-bg-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/full-bg-text.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/icon_journals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/icon_journals.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/dl_head_bg_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/dl_head_bg_2.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/gradient-bg-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/gradient-bg-demo.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/icon_daily_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/icon_daily_plan.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/icon_relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/icon_relations.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/light_spots_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/light_spots_bg.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/rtc-abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/rtc-abstraction.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/whiteboard-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/whiteboard-demo.png -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.md": [ "parcel-transformer-markdown", "..."] 5 | } 6 | } -------------------------------------------------------------------------------- /src/pages/Downloads/assets/icon_google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/icon_google_play.png -------------------------------------------------------------------------------- /src/pages/Downloads/assets/icon_windows_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Downloads/assets/icon_windows_64.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/full-bg-c-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/full-bg-c-gradient.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/icon_data_control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/icon_data_control.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/product_hunt_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/product_hunt_logo.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/filesync-abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/filesync-abstraction.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/footer-gradients-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/footer-gradients-bg.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/full-bg-text_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/full-bg-text_mobile.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/light_spots_full_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/light_spots_full_bg.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/light_spots_full_bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/light_spots_full_bg2.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/light_spots_top_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/light_spots_top_arrow.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/sync-indicator-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/sync-indicator-demo.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/whiteboard-demo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/whiteboard-demo-small.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/app-window-placeholder-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/app-window-placeholder-bg.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/whiteboard_canvas_fadeout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/whiteboard_canvas_fadeout.png -------------------------------------------------------------------------------- /src/pages/Landing/assets/app-window-placeholder-bg-no-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/sites/master/src/pages/Landing/assets/app-window-placeholder-bg-no-logo.png -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "sites" 2 | main = "index.ts" 3 | compatibility_date = "2022-09-29" 4 | 5 | [vars] 6 | ENVIRONMENT = "dev" 7 | 8 | [env.production.vars] 9 | ENVIRONMENT = "production" -------------------------------------------------------------------------------- /public/auth_callback.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.markedrc: -------------------------------------------------------------------------------- 1 | { 2 | "marked": { 3 | "breaks": true, 4 | "pedantic": false, 5 | "mangle": false, 6 | "headerIds": false, 7 | "gfm": true, 8 | "tables": true, 9 | "sanitize": false, 10 | "smartLists": false, 11 | "smartypants": false, 12 | "xhtml": false 13 | } 14 | } -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react' 2 | 3 | const useMounted = (): RefObject => { 4 | 5 | const mounted = useRef(false) 6 | 7 | useEffect(() => { 8 | mounted.current = true 9 | return () => { 10 | mounted.current = false 11 | } 12 | }, []) 13 | 14 | return mounted 15 | 16 | } 17 | 18 | export { 19 | useMounted, 20 | } 21 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import { App } from './App' 5 | import { Authenticator } from '@aws-amplify/ui-react' 6 | 7 | const root = createRoot(document.querySelector('#root')!) 8 | 9 | root.render( 10 | 11 | 12 | 13 | 14 | , 15 | ) 16 | -------------------------------------------------------------------------------- /src/pages/Landing/assets/logo-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pages/User/Terms.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import termsHTML from './terms.md' 3 | import { LandingFooterNav } from '../Landing' 4 | 5 | export function TermsContent() { 6 | return ( 7 |
) 10 | } 11 | 12 | export function TermsPage() { 13 | return ( 14 |
15 | {TermsContent()} 16 | 17 | {/* global footer */} 18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /src/components/Cards.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import cx from 'classnames' 3 | 4 | export const GlassCard = React.forwardRef(( 5 | props, ref, 6 | ) => { 7 | const { children, className, delay, animation, ...rest } = props 8 | 9 | const [animateClass, setAnimateClass] = useState('') 10 | 11 | useEffect(() => { 12 | setTimeout(() => { 13 | setAnimateClass(`ani-${animation || 'slide-in-from-bottom'}`) 14 | }, delay || 1000) 15 | }, []) 16 | 17 | return ( 18 |
19 |
20 | {children} 21 |
22 |
23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /src/pages/User/PrivacyPolicy.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import html from './privacy_policy.md' 3 | import { LandingFooterNav } from '../Landing' 4 | 5 | export function PrivacyPolicyContent() { 6 | return ( 7 |
) 10 | } 11 | 12 | export function PrivacyPolicyPage() { 13 | return ( 14 |
15 | {PrivacyPolicyContent()} 16 | 17 | {/* global footer */} 18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type IProInfo = { 2 | FileSyncExpireAt: number 3 | FileSyncGraphCountLimit: { 4 | 'current': number, 5 | 'free': number, 6 | 'pro': number, 7 | }, 8 | FileSyncStorageLimit: { 9 | 'current': number, 10 | 'currentFormatted': string, 11 | 'free': number, 12 | 'freeFormatted': string, 13 | 'pro': number, 14 | 'proFormatted': string 15 | }, 16 | ProUser: boolean 17 | UserGroups: 'alpha-tester' | string 18 | 19 | FreeTrialEndsAt: { ['LogseqPro']: string } 20 | LemonSubscriptionID: { ['LogseqPro']: string } 21 | LemonCustomerID: { ['LogseqPro']: string } 22 | LemonEndsAt: { ['LogseqPro']: string } 23 | LemonRenewsAt: { ['LogseqPro']: string } 24 | LemonStatus: { ['LogseqPro']: string } 25 | } 26 | 27 | declare global { 28 | interface Window { 29 | LemonSqueezy: any 30 | createLemonSqueezy: () => void 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | import { XCircle } from '@phosphor-icons/react' 3 | import { useRef } from 'react' 4 | 5 | export function Modal (props: any) { 6 | const { id, visible, destroy, className, hasClose, children, onClick, ...rest } = props 7 | const refInner = useRef() 8 | 9 | return ( 10 |
{ 12 | if (target && refInner.current?.contains(target as any)) { 13 | return 14 | } 15 | 16 | destroy() 17 | }} 18 | 19 | {...rest}> 20 |
21 |
22 | {children} 23 |
24 | 25 | {(hasClose !== false) && ( 26 | 29 | 30 | 31 | )} 32 |
33 |
34 | ) 35 | } -------------------------------------------------------------------------------- /src/pages/Landing/index.ts: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import { HeadShowcase } from './HeadShowcase' 4 | import { TutorialShowcase } from './TutorialShowcase' 5 | import { TutorialTips } from './TutorialTips' 6 | import { LandingFooterDesc, LandingFooterNav } from './LandingFooterNav' 7 | 8 | const fullBgImageB: any = new URL('./assets/full-bg-text.png', import.meta.url) 9 | const fullBgImageBMobile: any = new URL('./assets/full-bg-text_mobile.png', import.meta.url) 10 | 11 | const promiseImages: any = { 12 | '00': new URL('./assets/promise-0-0.png', import.meta.url), 13 | '01': new URL('./assets/promise-0-1.png', import.meta.url), 14 | '10': new URL('./assets/promise-1-0.png', import.meta.url), 15 | '11': new URL('./assets/promise-1-1.png', import.meta.url), 16 | '20': new URL('./assets/promise-2-0.png', import.meta.url), 17 | '21': new URL('./assets/promise-2-1.png', import.meta.url), 18 | } 19 | 20 | export { 21 | HeadShowcase, 22 | TutorialShowcase, 23 | TutorialTips, 24 | LandingFooterNav, 25 | LandingFooterDesc, 26 | fullBgImageB, 27 | fullBgImageBMobile, 28 | promiseImages, 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Logseq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/Animations.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useInView } from 'react-intersection-observer' 3 | 4 | export function delay (ms = 1000) { 5 | return new Promise((resolve) => { 6 | setTimeout(resolve, ms) 7 | }) 8 | } 9 | 10 | export function AnimateInTurnStage ( 11 | props: any 12 | ) { 13 | let { ani, ticks, children, ...rest } = props 14 | const [turnState, setTurnState] = useState([]) 15 | const { ref, inView, entry } = useInView({ 16 | threshold: 0 17 | }) 18 | 19 | children = children(turnState) 20 | 21 | useEffect(() => { 22 | const len = children.props?.children?.length 23 | if (!len) return 24 | 25 | const run = async () => { 26 | for (let i = 0; i < len; i++) { 27 | if (inView) { 28 | await delay(ticks?.[i] || 200) 29 | } 30 | 31 | turnState[i] = inView ? (ani || true) : false 32 | setTurnState([...turnState]) 33 | } 34 | } 35 | 36 | if (turnState.length || inView) { 37 | run().catch(console.error) 38 | } 39 | }, [inView]) 40 | 41 | return ( 42 |
43 | {children} 44 |
45 | ) 46 | } -------------------------------------------------------------------------------- /src/pages/User/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import { LoginContent } from './Login' 3 | import { authConfig, useAppState } from '../../state' 4 | import { setupAuthConfigure } from './amplify' 5 | import { LandingFooterNav } from '../Landing' 6 | import { AccountContent } from './Account' 7 | import { useLocation } from 'react-router-dom' 8 | 9 | // setup amplify configures 10 | // setupAuthConfigure(authConfig) 11 | 12 | function UserEntryPage () { 13 | const appState = useAppState() 14 | const userInfo = appState.userInfo.get({ noproxy: true }) 15 | const location = useLocation() 16 | const isLoginPath = location.pathname === '/login' 17 | 18 | let content = <> 19 | 20 | if (userInfo?.username && !isLoginPath) { 21 | content = 22 | } else { 23 | content = 24 | } 25 | 26 | return ( 27 |
28 |
29 | {content} 30 |
31 | 32 | {/* global footer */} 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | export { 45 | UserEntryPage, 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/deploy-s3.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | workflow_dispatch: 7 | 8 | env: 9 | NODE_VERSION: "20" 10 | PUBLIC_URL: "https://logseq.com/" 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ env.NODE_VERSION }} 25 | cache: "yarn" 26 | 27 | - name: yarn install, build 28 | run: | 29 | yarn install 30 | yarn build 31 | 32 | - name: Upload dist to S3 33 | run: aws s3 sync dist "s3://${{ env.AWS_S3_BUCKET }}" --delete 34 | env: 35 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 36 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 37 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 38 | AWS_REGION: us-east-1 39 | 40 | - name: Invalidate cache 41 | run: aws cloudfront create-invalidation --distribution-id "${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }}" --paths "/*" 42 | env: 43 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 44 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 45 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 46 | AWS_REGION: us-east-1 47 | -------------------------------------------------------------------------------- /src/components/Buttons.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import cx from 'classnames' 3 | 4 | export interface ILSButtonProps extends Omit, 'type'> { 5 | leftIcon?: ReactNode 6 | rightIcon?: ReactNode 7 | href?: string 8 | asAnchor?: boolean 9 | } 10 | 11 | export const Button = React.forwardRef(( 12 | props, ref 13 | ) => { 14 | const { asAnchor, href, leftIcon, rightIcon, children, className, ...rest } = props 15 | const rootProps: any = { 16 | ref: ref, 17 | className: cx( 18 | 'flex items-center justify-between text-base space-x-1 bg-logseq-400 rounded-lg py-3 px-4 whitespace-nowrap', 19 | 'transition-opacity hover:opacity-80 active:opacity-100 disabled:hover:opacity-100', 20 | className), 21 | } 22 | 23 | if (href && !asAnchor) { 24 | rootProps.onClick = () => { 25 | window?.open( 26 | href, '_blank' 27 | ) 28 | } 29 | } 30 | 31 | const inner = ( 32 | <> 33 |
34 | {leftIcon && {leftIcon}} 35 | {children} 36 |
37 | 38 | {rightIcon && {rightIcon}} 39 | 40 | ) 41 | 42 | if (asAnchor) { 43 | return ( 44 | 45 | {inner} 46 | ) 47 | } 48 | 49 | return ( 50 | 53 | ) 54 | }) 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sites", 3 | "version": "0.0.1", 4 | "description": "Logseq web site project.", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "rm -rf .parcel-cache && parcel ./index.html --public-url http://localhost:1234/", 8 | "build": "rm -rf ./dist && parcel build ./index.html --public-url $PUBLIC_URL && mkdir -p ./dist/public && cp -rf ./public/* ./dist/public", 9 | "dev:cloudflare": "wrangler2 dev" 10 | }, 11 | "devDependencies": { 12 | "@cloudflare/workers-types": "4.20230807.0", 13 | "@types/cookie": "^0.5.1", 14 | "@types/js-cookie": "3.0.3", 15 | "@types/react": "18.2.20", 16 | "@types/react-dom": "18.2.7", 17 | "buffer": "6.0.3", 18 | "core-js-pure": "^3.32.1", 19 | "events": "^3.1.0", 20 | "parcel": "2.9.3", 21 | "parcel-transformer-markdown": "^3.0.0", 22 | "postcss": "8.4.27", 23 | "postcss-nesting": "12.0.1", 24 | "process": "^0.11.10", 25 | "prop-types": "^15.8.1", 26 | "punycode": "2.3.0", 27 | "querystring-es3": "^0.2.1", 28 | "tailwindcss": "3.3.3", 29 | "tailwindcss-animate": "1.0.6", 30 | "tailwindcss-border-gradient-radius": "^3.0.1", 31 | "typescript": "4.9.4", 32 | "url": "^0.11.0", 33 | "wrangler": "3.5.0" 34 | }, 35 | "dependencies": { 36 | "@aws-amplify/ui": "^5.7.2", 37 | "@aws-amplify/ui-react": "5.3.0", 38 | "@hookstate/core": "4.0.1", 39 | "@phosphor-icons/react": "2.0.10", 40 | "aws-amplify": "5.3.7", 41 | "camelcase": "8.0.0", 42 | "classnames": "2.3.2", 43 | "cookie": "^0.5.0", 44 | "copy-to-clipboard": "3.3.3", 45 | "js-cookie": "3.0.5", 46 | "photoswipe": "5.3.8", 47 | "platform-detect": "3.0.1", 48 | "react": "18.2.0", 49 | "react-avatar": "5.0.3", 50 | "react-datepicker": "4.17.0", 51 | "react-dom": "^18.2.0", 52 | "react-hot-toast": "2.4.1", 53 | "react-intersection-observer": "9.5.2", 54 | "react-router-dom": "6.15.0", 55 | "react-spinners": "0.13.8", 56 | "swiper": "10.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from '@cloudflare/workers-types' 2 | import cookie from 'cookie' 3 | 4 | export interface Env { 5 | // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ 6 | // MY_KV_NAMESPACE: KVNamespace; 7 | // 8 | // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ 9 | // MY_DURABLE_OBJECT: DurableObjectNamespace; 10 | // 11 | // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ 12 | // MY_BUCKET: R2Bucket; 13 | 14 | ENVIRONMENT: string 15 | } 16 | 17 | declare const ENVIRONMENT: string 18 | 19 | export default { 20 | async fetch ( 21 | request: Request, 22 | env: Env, 23 | ctx: ExecutionContext 24 | ): Promise { 25 | const { ENVIRONMENT } = env 26 | const isProd = ENVIRONMENT === 'production' 27 | const urlObj = new URL(request.url) 28 | const cookieObj = cookie.parse(request.headers.get('Cookie') || '') 29 | const justGoToApp = cookieObj && cookieObj[`spa`] == '1' 30 | 31 | console.log('D:', urlObj.pathname) 32 | console.log('D:', JSON.stringify(env)) 33 | 34 | const landingEntryPoint = isProd ? `https://logseq-sites.pages.dev` : 35 | `http://127.0.0.1:1234` 36 | const appEntryPoint = `https://logseq.com/?spa=true` 37 | const appAssetsPoint = `https://logseq.com` 38 | 39 | if (['/', '/index.html'].includes(urlObj.pathname)) { 40 | const entryHtml = await (await fetch( 41 | justGoToApp ? appEntryPoint : landingEntryPoint 42 | )).text() 43 | return new Response(entryHtml, { 44 | headers: { 45 | 'content-type': 'text/html;charset=UTF-8' 46 | } 47 | }) 48 | } 49 | 50 | const forceAppEndpoint = [ 51 | `/js/worker.js`, 52 | `/js/magic_portal.js`, 53 | `/js/lightning-fs.min.js` 54 | ].some(it => { 55 | return urlObj.pathname?.startsWith(it) 56 | }) 57 | 58 | // TODO: just return object 59 | return fetch(`${forceAppEndpoint ? appAssetsPoint : landingEntryPoint}${urlObj.pathname}`) 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /src/components/PrivacyBanner.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from './Buttons' 2 | import { XCircle } from '@phosphor-icons/react' 3 | import { useEffect, useState } from 'react' 4 | import Cookie from 'js-cookie' 5 | 6 | export function PrivacyBanner() { 7 | const [ready, setReady] = useState(false) 8 | 9 | useEffect(() => { 10 | if ( 11 | sessionStorage?.getItem('__pb') == '1' || 12 | localStorage?.getItem('__pb') == '1' 13 | ) return 14 | 15 | setTimeout(() => { 16 | setReady(true) 17 | }, 1500) 18 | }, []) 19 | 20 | const closeBannerSession = () => { 21 | sessionStorage?.setItem('__pb', '1') 22 | setReady(false) 23 | } 24 | 25 | const acceptHandler = () => { 26 | localStorage?.setItem('__pb', '1') 27 | Cookie.set(`__pb`, '1', { expires: 7 }) 28 | setReady(false) 29 | } 30 | 31 | const denyHandler = () => { 32 | closeBannerSession() 33 | } 34 | 35 | return ( 36 | <> 37 | {ready ? 38 | (
39 |
40 |
41 | By clicking “Accept All Cookies”, you agree to the storing of 42 | cookies 43 | on 44 | your device to enhance site navigation, and analyze site usage. 45 | View our Privacy Policy for more. 50 |
51 | 52 |
53 | 55 | 57 | 62 |
63 |
64 |
) : null 65 | } 66 | 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/Landing/common.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | 3 | // @ts-ignore 4 | import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js' 5 | 6 | export const imageS1: any = new URL('./assets/tutorials-1.png', import.meta.url) 7 | export const imageLogo: any = new URL('./assets/logo2.png', import.meta.url) 8 | export const imageLogoDots: any = new URL('./assets/logo-dots.svg', import.meta.url) 9 | export const imageProductHuntLogo: any = new URL('./assets/product_hunt_logo.png', import.meta.url) 10 | 11 | export function FloatGlassButton ( 12 | props: any, 13 | ) { 14 | const { href, children, className, ...rest } = props 15 | 16 | if (href) { 17 | rest.onClick = () => { 18 | window?.open( 19 | href, '_blank' 20 | ) 21 | } 22 | } 23 | 24 | return ( 25 | 28 | ) 29 | } 30 | 31 | export function AppLogo ( 32 | props: any 33 | ) { 34 | const { className, ...rest } = props 35 | 36 | return ( 37 | 38 | Logseq 39 | 40 | ) 41 | } 42 | 43 | export function AppLogoEmbossed ( 44 | props: any 45 | ) { 46 | const { className, ...rest } = props 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ) 57 | } 58 | 59 | 60 | export function openLightbox ( 61 | sources: Array<{ src: string, width: number, height: number }>, 62 | index: number = 0, 63 | ) { 64 | const lightbox = new PhotoSwipeLightbox({ 65 | dataSource: sources, 66 | wheelToZoom: true, 67 | pswpModule: () => import('photoswipe'), 68 | }) 69 | 70 | lightbox.init() 71 | lightbox.loadAndOpen(index) 72 | } 73 | -------------------------------------------------------------------------------- /src/pages/User/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Authenticator, CheckboxField, useAuthenticator, AccountSettings } from '@aws-amplify/ui-react' 2 | import { useLocation, useNavigate } from 'react-router-dom' 3 | import { applyLoginUser } from '../../state' 4 | 5 | function LSAuthenticator ({ termsLink, children }: any) { 6 | return ( 7 | 18 | {/*Re-use default `Authenticator.SignUp.FormFields` */} 19 | 20 | 21 | {/*Append & require Terms & Conditions field to sign up */} 22 | I agree with the Terms & Conditions)} 28 | /> 29 | 30 | ) 31 | } 32 | }, 33 | }} 34 | services={{ 35 | async validateCustomSignUp (formData) { 36 | if (!formData.acknowledgement) { 37 | return { 38 | acknowledgement: '', 39 | } 40 | } 41 | } 42 | }} 43 | > 44 | {children} 45 | ) 46 | } 47 | 48 | function LSAuthenticatorChangePassword ( 49 | { onSuccess, onError }: any 50 | ) { 51 | return ( 52 | 53 | ) 54 | } 55 | 56 | export function LoginContent () { 57 | const routeLocation = useLocation() 58 | const navigate = useNavigate() 59 | 60 | return ( 61 |
62 | 63 | {({ user }: any) => { 64 | setTimeout(() => { 65 | applyLoginUser(user, { navigate, routeLocation, inComponent: true }) 66 | }, 50) 67 | }} 68 | 69 |
) 70 | } 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Logseq: A privacy-first, open-source knowledge base 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /assets/particlesjs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "particles": { 3 | "number": { 4 | "value": 175, 5 | "density": { 6 | "enable": false, 7 | "value_area": 1578.2952832645453 8 | } 9 | }, 10 | "color": { 11 | "value": "#ffffff" 12 | }, 13 | "shape": { 14 | "type": "circle", 15 | "stroke": { 16 | "width": 0, 17 | "color": "#000000" 18 | }, 19 | "polygon": { 20 | "nb_sides": 4 21 | }, 22 | "image": { 23 | "src": "img/github.svg", 24 | "width": 100, 25 | "height": 100 26 | } 27 | }, 28 | "opacity": { 29 | "value": 0.04810236182596568, 30 | "random": false, 31 | "anim": { 32 | "enable": false, 33 | "speed": 0.32481242082697265, 34 | "opacity_min": 0.04060155260337158, 35 | "sync": false 36 | } 37 | }, 38 | "size": { 39 | "value": 3.945738208161363, 40 | "random": true, 41 | "anim": { 42 | "enable": false, 43 | "speed": 40, 44 | "size_min": 0.1, 45 | "sync": false 46 | } 47 | }, 48 | "line_linked": { 49 | "enable": true, 50 | "distance": 150, 51 | "color": "#ffffff", 52 | "opacity": 0.4, 53 | "width": 1 54 | }, 55 | "move": { 56 | "enable": true, 57 | "speed": 1.603412060865523, 58 | "direction": "none", 59 | "random": false, 60 | "straight": false, 61 | "out_mode": "out", 62 | "bounce": false, 63 | "attract": { 64 | "enable": false, 65 | "rotateX": 2004.2650760819035, 66 | "rotateY": 1202.559045649142 67 | } 68 | } 69 | }, 70 | "interactivity": { 71 | "detect_on": "canvas", 72 | "events": { 73 | "onhover": { 74 | "enable": false, 75 | "mode": "repulse" 76 | }, 77 | "onclick": { 78 | "enable": false, 79 | "mode": "repulse" 80 | }, 81 | "resize": true 82 | }, 83 | "modes": { 84 | "grab": { 85 | "distance": 400, 86 | "line_linked": { 87 | "opacity": 1 88 | } 89 | }, 90 | "bubble": { 91 | "distance": 400, 92 | "size": 40, 93 | "duration": 2, 94 | "opacity": 8, 95 | "speed": 3 96 | }, 97 | "repulse": { 98 | "distance": 200, 99 | "duration": 0.4 100 | }, 101 | "push": { 102 | "particles_nb": 4 103 | }, 104 | "remove": { 105 | "particles_nb": 2 106 | } 107 | } 108 | }, 109 | "retina_detect": true 110 | } -------------------------------------------------------------------------------- /src/pages/User/amplify.ts: -------------------------------------------------------------------------------- 1 | import './amplify.css' 2 | import { Amplify, I18n } from 'aws-amplify' 3 | import { translations } from '@aws-amplify/ui' 4 | 5 | // fix i18n 6 | translations.zh['Reset Password'] = '重置密码' 7 | translations.zh['Enter your username'] = '请输入用户名' 8 | translations.zh['Enter your email'] = '请输入邮箱' 9 | translations.zh['Enter your password'] = '请输入密码' 10 | translations.zh['Confirm Password'] = '确认密码' 11 | translations.zh['Please confirm your Password'] = '请确认密码' 12 | translations.zh['Incorrect username or password.'] = '用户名或者密码不正确。如果您的邮箱未验证,请尝试使用用户名(非邮箱)登录,以保证再次邮箱验证流程。' 13 | 14 | // @ts-ignore attach defaults 15 | translations.en = { 16 | 'Incorrect username or password.': 'Incorrect username or password! ' + 17 | 'For unconfirmed users, please input your username instead of Email to receive the code.' 18 | } 19 | 20 | const fixesMapping = { 21 | 'Sign Up': ['Sign up', 'Create Account'], 22 | 'Sign In': ['Sign in'], 23 | 'Sign Out': 'Sign out', 24 | 'Send Code': 'Send code', 25 | 'Forgot Password': ['Forgot your password?'], 26 | 'Enter your email': ['Enter your Email'], 27 | 'Enter your password': ['Enter your Password'], 28 | 'Enter your username': ['Enter your Username'] 29 | } 30 | 31 | Object.keys(translations).forEach((k) => { 32 | const target = translations[k] 33 | Object.entries(fixesMapping).forEach(([k1, v1]) => { 34 | if (target?.hasOwnProperty(k1)) { 35 | const vs = Array.isArray(v1) ? v1 : [v1] 36 | vs.forEach(it => { 37 | target[it] = target[k1] 38 | }) 39 | } 40 | }) 41 | }) 42 | 43 | I18n.putVocabularies(translations) 44 | 45 | export function setupAuthConfigure (config: any) { 46 | const { 47 | region, 48 | userPoolId, 49 | userPoolWebClientId, 50 | identityPoolId, 51 | oauthDomain, 52 | oauthProviders 53 | } = config 54 | 55 | Amplify.configure({ 56 | 'aws_project_region': region, 57 | 'aws_cognito_identity_pool_id': identityPoolId, 58 | 'aws_cognito_region': region, 59 | 'aws_user_pools_id': userPoolId, 60 | 'aws_user_pools_web_client_id': userPoolWebClientId, 61 | 'authenticationFlowType': 'USER_SRP_AUTH', 62 | 'oauth': { 63 | 'domain': oauthDomain, 64 | 'scope': [ 65 | 'phone', 66 | 'email', 67 | 'openid', 68 | 'profile', 69 | 'aws.cognito.signin.user.admin' 70 | ], 71 | 'redirectSignIn': 'https://logseq.com/public/auth_callback.html', 72 | 'redirectSignOut': 'https://logseq.com/public/auth_callback.html', 73 | 'responseType': 'code' 74 | }, 75 | 'federationTarget': 'COGNITO_USER_POOLS', 76 | 'aws_cognito_social_providers': oauthProviders || [ 77 | 'GOOGLE' 78 | ], 79 | 'aws_cognito_signup_attributes': [ 80 | 'EMAIL' 81 | ], 82 | 'aws_cognito_password_protection_settings': { 83 | 'passwordPolicyMinLength': 8, 84 | 'passwordPolicyCharacters': [] 85 | }, 86 | 'aws_cognito_verification_mechanisms': [ 87 | 'EMAIL' 88 | ] 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/pages/Downloads/index.css: -------------------------------------------------------------------------------- 1 | .app-head-downloads { 2 | @apply px-4 sm:px-0; 3 | 4 | .text-slogan { 5 | h2 { 6 | @apply sm:pt-10 sm:text-[38px] sm:leading-[46px]; 7 | } 8 | } 9 | 10 | .dl-items { 11 | @apply sm:px-10; 12 | 13 | > .tabs { 14 | li { 15 | @apply flex flex-col items-center bg-logseq-600/90 rounded-xl 16 | flex-1 py-5 px-2 justify-center select-none cursor-pointer 17 | active:opacity-80 border-2 border-transparent transition-all duration-300; 18 | 19 | &.active { 20 | @apply border-gradient-br-yellow-purple-red-logseq-900; 21 | } 22 | 23 | &:hover { 24 | box-shadow: 0 2px 18px rgba(73, 134, 190, 0.3), 25 | inset 0 0 7px rgba(237, 204, 114, 0.5); 26 | } 27 | 28 | &.is-macos, &.is-ios { 29 | &.active { 30 | @apply border-gradient-br-purple-white-red-logseq-900; 31 | } 32 | } 33 | 34 | &.is-windows { 35 | &.active { 36 | @apply border-gradient-br-blue-white-green-logseq-900; 37 | } 38 | } 39 | 40 | &.is-linux { 41 | &.active { 42 | @apply border-gradient-br-yellow-white-orange-logseq-900; 43 | } 44 | } 45 | 46 | &.is-android { 47 | &.active { 48 | @apply border-gradient-br-green-white-black-logseq-900; 49 | } 50 | } 51 | 52 | > span { 53 | font-size: 38px; 54 | } 55 | 56 | > strong { 57 | @apply text-lg font-normal opacity-70 text-center pt-4; 58 | } 59 | } 60 | } 61 | 62 | > .selects { 63 | .app-form-select { 64 | outline: none; 65 | } 66 | 67 | &.active-of-macos, &.active-of-ios { 68 | .app-form-select { 69 | @apply border-gradient-br-purple-white-red-logseq-900; 70 | } 71 | } 72 | 73 | &.active-of-linux .app-form-select { 74 | @apply border-gradient-br-yellow-white-orange-logseq-900; 75 | } 76 | 77 | &.active-of-windows .app-form-select { 78 | @apply border-gradient-br-blue-white-green-logseq-900; 79 | } 80 | 81 | &.active-of-android .app-form-select { 82 | @apply border-gradient-br-green-white-black-logseq-900; 83 | } 84 | } 85 | 86 | .panels { 87 | @apply sm:h-[170px]; 88 | 89 | @screen sm { 90 | > .is-ios { 91 | @apply relative; 92 | 93 | &:hover { 94 | .qr { 95 | @apply opacity-100; 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | .screen-shot { 104 | } 105 | } 106 | 107 | .page-inner-full-wrap { 108 | &.dl-a { 109 | @apply relative overflow-hidden; 110 | 111 | &:after { 112 | @apply bottom-0 left-0 h-28 w-full absolute bg-gradient-to-t from-logseq-800; 113 | content: " "; 114 | } 115 | } 116 | } 117 | 118 | .global-downloads-wrap { 119 | @apply whitespace-nowrap; 120 | 121 | &.is-super-button { 122 | .sub-items { 123 | top: 36px; 124 | right: unset !important; 125 | left: 0; 126 | width: 280px; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/components/Icons.tsx: -------------------------------------------------------------------------------- 1 | import { BeatLoader } from 'react-spinners' 2 | 3 | export function IconsIntel ( 4 | props: { 5 | size: number 6 | color?: string 7 | } 8 | ) { 9 | return ( 10 | 14 | 17 | 20 | 21 | ) 22 | } 23 | 24 | export function LSSpinner (props: any) { 25 | return ( 26 | 27 | ) 28 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./pages/User/index.css" 2 | import { Route, Routes, useLocation } from 'react-router-dom' 3 | import { HomePage } from './pages/Home' 4 | import { DownloadsPage } from './pages/Downloads' 5 | import { Headbar } from './components/Headbar' 6 | import { 7 | checkSmBreakPoint, 8 | useAppState, useAuthUserInfoState, 9 | useDiscordState, useModalsState, 10 | useReleasesState, 11 | } from './state' 12 | import React, { useEffect } from 'react' 13 | import { Toaster } from 'react-hot-toast' 14 | import { Modal } from './components/Modal' 15 | import { createPortal } from 'react-dom' 16 | import { scrollToTop } from './components/utils' 17 | import cx from 'classnames' 18 | import { TermsPage } from './pages/User/Terms' 19 | import { PrivacyPolicyPage } from './pages/User/PrivacyPolicy' 20 | 21 | export function App() { 22 | const appState = useAppState() 23 | const modalsState = useModalsState() 24 | const userInfoState = appState.userInfo 25 | const { pathname } = useLocation() 26 | const hasActiveModals = modalsState.modals?.some(m => m.value.visible) 27 | 28 | useEffect(() => { 29 | scrollToTop() 30 | }, [pathname]) 31 | 32 | // load global state 33 | useAuthUserInfoState() 34 | useReleasesState() 35 | useDiscordState() 36 | 37 | useEffect(() => { 38 | const handler = ({ which }: KeyboardEvent) => { 39 | // ESC 40 | const t = modalsState.topmost() 41 | if (which === 27 && t) { 42 | modalsState.remove(t.id) 43 | } 44 | } 45 | 46 | document.body.addEventListener('keyup', handler) 47 | return () => document.body.removeEventListener('keyup', handler) 48 | }, []) 49 | 50 | useEffect(() => { 51 | const resizeHandler = () => { 52 | appState.sm.set( 53 | checkSmBreakPoint(), 54 | ) 55 | } 56 | 57 | window.addEventListener('resize', resizeHandler) 58 | return () => { 59 | window.removeEventListener('resize', resizeHandler) 60 | } 61 | }, []) 62 | 63 | useEffect(() => { 64 | const container = document.documentElement 65 | container.classList[hasActiveModals ? 'add' : 'remove']('ui-modal-container-locked') 66 | }, [hasActiveModals]) 67 | 68 | return ( 69 |
70 | 76 | 77 |
78 | 79 | 80 | 81 | }/> 82 | }/> 83 | }> 84 | }> 85 | 86 | 87 | {/* modals */} 88 | {modalsState.modals.get({ noproxy: true })?.map(m => { 89 | return ( 90 | createPortal( 91 | modalsState.remove(m.id)} 94 | {...m.props} 95 | > 96 | {m.content}, 97 | document.body)) 98 | })} 99 |
100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { MutableRefObject, ReactElement, useEffect, useRef, useState } from 'react' 2 | import cx from 'classnames' 3 | 4 | type MenuDataItem = { 5 | key: string | number 6 | text?: string | ReactElement 7 | type?: string 8 | props?: Record 9 | 10 | render?: ((e?: any) => ReactElement) | ReactElement 11 | } 12 | 13 | export const Dropdown = React.forwardRef, 15 | [k: string]: any 16 | }>>((props, ref) => { 17 | const { className, children, wrapItems, subItemClassName, items, triggerType, ...dropdownProps } = props 18 | const triggerProps: any = {} 19 | const [active, setActive] = useState(false) 20 | 21 | ref = ref || useRef(null) 22 | 23 | const triggerClickable = triggerType === 'click' 24 | 25 | if (triggerClickable) { 26 | triggerProps.onClick = () => setActive(!active) 27 | 28 | const _onClick = dropdownProps.onClick 29 | 30 | dropdownProps.onClick = (e: MouseEvent) => { 31 | const target = e.target as HTMLElement 32 | if (!target?.closest('.sub-items')) return 33 | setActive(false) 34 | _onClick?.(e) 35 | } 36 | } else { 37 | dropdownProps.onMouseEnter = () => setActive(true) 38 | dropdownProps.onMouseLeave = () => setActive(false) 39 | } 40 | 41 | // outside click effect 42 | useEffect(() => { 43 | const host = document.documentElement 44 | const handler = (e: MouseEvent) => { 45 | const target = e.target 46 | // @ts-ignore 47 | if (ref?.current?.contains(target)) return 48 | setActive(false) 49 | } 50 | 51 | if (triggerClickable) { 52 | host.addEventListener('mouseup', handler, false) 53 | } else { 54 | host.removeEventListener('mouseup', handler) 55 | } 56 | 57 | return () => host.removeEventListener('mouseup', handler) 58 | }, [triggerClickable]) 59 | 60 | const wrapSubItems = (children: ReactElement) => ( 61 | (wrapItems === false ? children : 62 |
65 |
66 | {children} 67 |
68 |
)) 69 | 70 | return ( 71 |
75 | 78 | {children} 79 | 80 | 81 | {React.isValidElement(items) ? wrapSubItems(items) : 82 | (items?.length > 0 && 83 | wrapSubItems( 84 | items.map((it: MenuDataItem, idx: number) => { 85 | const { className, ...rest } = it.props || {} 86 | 87 | if (it.render) return ( 88 | 89 | {typeof it.render === 'function' ? (it.render(it)) : it.render} 90 | ) 91 | 92 | return ( 93 | 94 | {it.text} 95 | 96 | ) 97 | }) as any))} 98 |
99 | ) 100 | }) -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | fullBgImageB, fullBgImageBMobile, 3 | HeadShowcase, LandingFooterDesc, 4 | LandingFooterNav, promiseImages, 5 | TutorialShowcase, 6 | TutorialTips, 7 | } from './Landing' 8 | import { AnimateInTurnStage } from '../components/Animations' 9 | import cx from 'classnames' 10 | import { DailyShowcase } from './Landing/DailyShowcase' 11 | import { useAppState } from '../state' 12 | 13 | export function HomePage () { 14 | const appState = useAppState() 15 | 16 | // useEffect(() => { 17 | // setTimeout(() => { 18 | // // @ts-ignore 19 | // particlesJS.load('particles-bg', './particlesjs-config.json', () => { 20 | // }) 21 | // }, 1000) 22 | // }, []) 23 | 24 | return ( 25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 | {appState.sm.get() ? 35 | image : 36 | image 37 | } 38 | 39 | {/* text slogan */} 40 | 43 | {(t: Array) => { 44 | return ( 45 | <> 46 |

51 | Overwhelmed and constantly 53 | afraid of losing your 54 | thoughts? 55 |

56 | 57 |

62 | Everyday you’re bombarded with information. 63 | Your non-connected notes lead to missing context when 64 | 65 | you need it. 66 | That gets future-you into trouble. 67 | 68 |

69 | 70 | ) 71 | }} 72 | 73 |
74 |
75 |
76 | 77 |
78 | 79 |
80 | 81 |
82 | {/* particles background */} 83 | {/*
*/} 84 | 85 |
86 | 87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 |
95 |
96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors') 2 | 3 | module.exports = { 4 | content: [ 5 | './src/**/*.{html,js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'logseq': { 11 | 50: '#b7d5d5', 12 | 100: '#8EC2C2', 13 | 200: '#2c7d8f', 14 | 300: '#266C7D', 15 | 400: '#195D6C', 16 | 500: '#094b5a', 17 | 600: '#023643', 18 | 700: '#002B36', 19 | 800: '#01222a', 20 | 900: '#01171d' 21 | }, 22 | 'pro': { 23 | 50: '#F5FEFB', 24 | 100: '#cde9ff', 25 | 200: '#A6E0FA', 26 | 300: '#7ECDF6', 27 | 400: '#3CBAF3', 28 | 500: '#0EA5E9', 29 | 600: '#0C8DC7', 30 | 700: '#0A75A6', 31 | 800: '#085D84', 32 | 900: '#064662' 33 | } 34 | }, 35 | fontSize: { 36 | '4xl': '2.4rem', 37 | '6xl': '4rem' 38 | }, 39 | backdropBlur: { 40 | '4xl': '80px', 41 | '5xl': '100px' 42 | }, 43 | backgroundImage: { 44 | 'radial': 'radial-gradient(var(--tw-gradient-stops))' 45 | }, 46 | linearBorderGradients: ({ theme }) => ({ 47 | colors: { 48 | 'yellow-purple-red': [ 49 | colors.amber[400], 50 | colors.purple[500], 51 | colors.red[600] 52 | ], 53 | 54 | 'red-purple-blue': [ 55 | colors.fuchsia[400], 56 | colors.purple[500], 57 | colors.blue[600] 58 | ], 59 | 60 | 'yellow-brown-yellow': [ 61 | colors.yellow[400], 62 | colors.stone[300], 63 | colors.yellow[700] 64 | ], 65 | 66 | 'red-black-yellow': [ 67 | colors.fuchsia[500], 68 | colors.stone[700], 69 | colors.yellow[400] 70 | ], 71 | 72 | 'green-black-cyan': [ 73 | colors.green[500], 74 | colors.stone[500], 75 | colors.cyan[600] 76 | ], 77 | 78 | 'gray-white-gray': [ 79 | colors.stone[400], 80 | colors.white, 81 | colors.stone[600] 82 | ], 83 | 'purple-white-red': [ 84 | colors.red[300], 85 | colors.white, 86 | colors.purple[400] 87 | ], 88 | 'blue-white-green': [ 89 | colors.blue[400], 90 | colors.white, 91 | colors.green[200] 92 | ], 93 | 'yellow-white-orange': [ 94 | colors.yellow[200], 95 | colors.white, 96 | colors.orange[400] 97 | ], 98 | 'green-white-black': [ 99 | colors.green[500], 100 | colors.white, 101 | colors.yellow[100] 102 | ], 103 | 104 | 'brown-white-blue': [ 105 | colors.yellow[300], 106 | colors.white, 107 | colors.blue[400] 108 | ], 109 | 'gray-white-blue': [ 110 | colors.stone[500], 111 | colors.white, 112 | colors.yellow[900] 113 | ] 114 | }, 115 | background: theme('colors'), 116 | }) 117 | }, 118 | }, 119 | plugins: [ 120 | require('tailwindcss-animate'), 121 | require('tailwindcss-border-gradient-radius') 122 | ], 123 | } 124 | -------------------------------------------------------------------------------- /src/components/utils.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | import Cookie from 'js-cookie' 4 | 5 | export type FunctionArguments = T extends (...args: infer R) => any ? R : never; 6 | 7 | export type Dict = Record; 8 | 9 | export type StringOrNumber = string | number; 10 | 11 | export function isNumber (value: any): value is number { 12 | return typeof value === 'number' 13 | } 14 | 15 | export function isArray (value: any): value is Array { 16 | return Array.isArray(value) 17 | } 18 | 19 | export function isFunction (value: any): value is Function { 20 | return typeof value === 'function' 21 | } 22 | 23 | export const isObject = (value: any): value is Dict => { 24 | const type = typeof value 25 | return value != null && (type === 'object' || type === 'function') && !isArray(value) 26 | } 27 | 28 | export const isNull = (value: any): value is null => value == null 29 | 30 | export function isDateValid (value: number | string | Date) { 31 | // @ts-ignore 32 | return !isNaN(new Date(value)) 33 | } 34 | 35 | export function callAll (...fns: any[]) { 36 | return function mergedFn (...args: any[]) { 37 | fns.forEach((fn) => { 38 | if (isFunction(fn)) { 39 | fn?.(...args) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | export function mergeProps (...args: T) { 46 | const result: Dict = {} 47 | 48 | for (const props of args) { 49 | for (const key in result) { 50 | if (/^on[A-Z]/.test(key) && typeof result[key] === 'function' && typeof props[key] === 'function') { 51 | result[key] = callAll(result[key], props[key]) 52 | } else if (key === 'className' && typeof result.className === 'string' && typeof props.className === 'string') { 53 | result[key] = cx(result.className, props.className) 54 | } else { 55 | result[key] = props[key] !== undefined ? props[key] : result[key] 56 | } 57 | } 58 | 59 | // Add props from b that are not in a 60 | for (const key in props) { 61 | if (result[key] === undefined) { 62 | result[key] = props[key] 63 | } 64 | } 65 | } 66 | 67 | return result 68 | } 69 | 70 | export const openLiveDemo = () => { 71 | Cookie.set(`spa`, '1', { expires: 7 }) 72 | setTimeout(() => window.location.reload(), 500) 73 | } 74 | 75 | export function slugify (input: string | undefined) { 76 | return input?.replace(/\W/g, '_') 77 | } 78 | 79 | export function navigateTabs (event: React.KeyboardEvent) { 80 | const target = event.target as HTMLElement 81 | const key = event.key 82 | const tabs = Array.prototype.slice.call(target?.closest('[role="tablist"]')?.querySelectorAll('[role="tab"]')) 83 | let index = tabs.indexOf(target) 84 | 85 | if (key.includes('Arrow')) { 86 | switch (key) { 87 | case 'ArrowLeft': 88 | case 'ArrowUp': 89 | index = index > 0 ? index - 1 : tabs.length - 1 90 | break 91 | case 'ArrowRight': 92 | case 'ArrowDown': 93 | index = index < tabs.length - 1 ? index + 1 : 0 94 | break 95 | } 96 | 97 | tabs[index].click() 98 | tabs[index].focus() 99 | event.preventDefault() 100 | } 101 | } 102 | 103 | export function scrollToTop () { 104 | document.documentElement.scrollTop = 0 105 | } 106 | 107 | export function bytesFormat (bytes: number) { 108 | if (!isNumber(bytes)) return '0 B' 109 | const giga = bytes / 1024 / 1024 / 1024 110 | return giga >= 0.5 ? `${giga}GB` : `${giga * 1024}MB` 111 | } -------------------------------------------------------------------------------- /src/pages/User/index.css: -------------------------------------------------------------------------------- 1 | [data-amplify-authenticator] [data-amplify-router] { 2 | --amplify-colors-brand-primary-10: #b7d5d5; 3 | --amplify-colors-brand-primary-20: #8EC2C2; 4 | --amplify-colors-brand-primary-40: #2c7d8f; 5 | --amplify-colors-brand-primary-60: #266C7D; 6 | --amplify-colors-brand-primary-80: #094b5a; 7 | --amplify-colors-brand-primary-90: #023643; 8 | --amplify-colors-brand-primary-100: #01222a; 9 | --amplify-components-text-error-color: #be3333; 10 | --amplify-components-fieldcontrol-error-border-color: #be3333; 11 | --amplify-components-authenticator-form-padding: 40px; 12 | --amplify-components-authenticator-footer-padding-bottom: 25px; 13 | 14 | --amplify-components-heading-color: var(--amplify-colors-brand-primary-10); 15 | --amplify-components-tabs-item-active-color: var(--amplify-colors-brand-primary-20); 16 | --amplify-components-tabs-item-hover-color: var(--amplify-colors-brand-primary-20); 17 | --amplify-components-tabs-item-focus-color: var(--amplify-colors-brand-primary-20); 18 | --amplify-components-button-color: var(--amplify-colors-brand-primary-20); 19 | 20 | --amplify-components-text-color: var(--amplify-colors-brand-primary-10); 21 | --amplify-components-field-label-color: var(--amplify-colors-brand-primary-10); 22 | --amplify-components-authenticator-router-background-color: transparent; 23 | --amplify-components-authenticator-state-inactive-background-color: #cde9ff; 24 | 25 | border-radius: 8px; 26 | overflow: hidden; 27 | 28 | .amplify-icon > svg { 29 | color: var(--amplify-colors-brand-primary-10); 30 | } 31 | } 32 | 33 | .amplify-input { 34 | color: var(--amplify-colors-brand-primary-10) !important; 35 | } 36 | 37 | [data-amplify-authenticator] [data-amplify-form] { 38 | padding-bottom: 20px !important; 39 | } 40 | 41 | [data-amplify-authenticator] { 42 | @apply relative top-[-55px]; 43 | 44 | .amplify-input:focus { 45 | --amplify-components-fieldcontrol-focus-border-color: #07708a; 46 | box-shadow: 0 0 3px #007d9c; 47 | } 48 | } 49 | 50 | .amplify-tabs[data-indicator-position="top"] { 51 | border: 0 !important; 52 | } 53 | 54 | [data-amplify-authenticator] [data-state="inactive"] { 55 | opacity: .7; 56 | } 57 | 58 | form[data-amplify-authenticator-signup] { 59 | .amplify-field { 60 | @apply mb-4; 61 | } 62 | 63 | fieldset { 64 | margin-bottom: 10px; 65 | } 66 | } 67 | 68 | form[data-amplify-authenticator-signin] { 69 | fieldset { 70 | @apply mb-3; 71 | } 72 | } 73 | 74 | form[data-amplify-authenticator-confirmsignup] { 75 | .amplify-field-group__control:last-child { 76 | margin-top: 12px; 77 | margin-bottom: 16px; 78 | } 79 | } 80 | 81 | .app-page.user-entry { 82 | @apply pt-[100px] w-full; 83 | 84 | .login-pane { 85 | @apply min-h-[90vh] w-full p-10 flex items-center justify-center; 86 | } 87 | } 88 | 89 | .federated-sign-in-container { 90 | display: none !important; 91 | } 92 | 93 | .app-account { 94 | .pro-flag { 95 | @apply border-[2px] border-pro-400 text-pro-400 rounded-md px-2 py-[3px] 96 | tracking-widest bg-pro-600/40 text-[12px] leading-none font-bold mx-1; 97 | } 98 | } 99 | 100 | .logseq-terms-content { 101 | @apply px-8 py-10 max-w-[1040px] text-logseq-50; 102 | 103 | > h1 { 104 | @apply text-3xl font-bold mb-8; 105 | } 106 | 107 | > p { 108 | @apply mb-2; 109 | } 110 | 111 | > h4 { 112 | @apply text-lg font-bold mt-6 mb-2; 113 | } 114 | 115 | pre { 116 | @apply whitespace-pre-wrap mb-3 py-4 px-4 bg-logseq-700 rounded-md; 117 | } 118 | 119 | table { 120 | @apply w-full border-collapse my-3; 121 | 122 | td, th { 123 | @apply border border-logseq-600 p-4; 124 | } 125 | } 126 | 127 | > p { 128 | @apply my-3; 129 | } 130 | } 131 | 132 | .logseq-iframe-content { 133 | > iframe { 134 | @apply mx-auto my-10 md:w-[760px] min-h-[72vh] bg-transparent; 135 | } 136 | } -------------------------------------------------------------------------------- /src/components/Headbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, NavLink, useSearchParams } from 'react-router-dom' 2 | import { ArrowSquareOut, List, X } from '@phosphor-icons/react' 3 | import { ReactElement, useEffect, useState } from 'react' 4 | import { WrapGlobalDownloadButton } from '../pages/Downloads' 5 | import cx from 'classnames' 6 | 7 | const logo: any = new URL('/assets/logo-with-border.png', import.meta.url) 8 | 9 | export function LinksGroup( 10 | props: { 11 | items: Array<{ link: string, label: string | ReactElement, icon?: ReactElement }>, 12 | 13 | [k: string]: any 14 | }, 15 | ) { 16 | const { items, className, ...rest } = props 17 | 18 | return ( 19 |
    22 | {items.map(it => { 23 | const inner = ( 24 | <> 25 | {it.label} 26 | {it.icon && <> 27 | {it.icon} 28 | (opens a new window) 29 | } 30 | ) 31 | 32 | return ( 33 |
  • 37 | {it.link.startsWith('http') 38 | ? 39 | {inner} 41 | : 42 | { 45 | return cx('h-full flex items-center group transition-colors', 46 | isActive && 'app-link-active') 47 | }}>{inner} 48 | } 49 | 50 |
  • 51 | ) 52 | })} 53 |
54 | ) 55 | } 56 | 57 | export function Headbar() { 58 | const [rightActive, setRightActive] = useState(false) 59 | const [urlParams] = useSearchParams() 60 | 61 | // without header 62 | if (urlParams.has('x')) return 63 | 64 | useEffect(() => { 65 | const outsideHandler = (e: MouseEvent) => { 66 | const target = e.target as any 67 | const isToggle = !!target.closest('a.nav-toggle') 68 | 69 | if (isToggle) { 70 | return setRightActive(!rightActive) 71 | } 72 | 73 | rightActive && setRightActive(false) 74 | } 75 | 76 | document.body.addEventListener('click', outsideHandler) 77 | 78 | return () => { 79 | document.body.removeEventListener('click', outsideHandler) 80 | } 81 | }, [rightActive]) 82 | 83 | useEffect(() => { 84 | if (rightActive) { 85 | document.body.classList.add('is-nav-open') 86 | } else { 87 | document.body.classList.remove('is-nav-open') 88 | } 89 | }, [rightActive]) 90 | 91 | const leftLinks = [ 92 | { label: 'Home', link: '/' }, 93 | // { 94 | // label: (<> 95 | // Pro 96 | // New 97 | // ), link: '/pro' 98 | // }, 99 | { label: 'Downloads', link: '/downloads' }, 100 | ] 101 | 102 | const rightLinks = [ 103 | { 104 | label: 'Forum', 105 | link: 'https://discuss.logseq.com', 106 | icon: , 107 | }, 108 | { 109 | label: 'Docs', 110 | link: 'https://docs.logseq.com/', 111 | icon: , 112 | }, 113 | { 114 | label: 'Github', 115 | link: 'https://github.com/logseq', 116 | icon: , 117 | }, 118 | { 119 | label: 'Blog', 120 | link: 'https://blog.logseq.com', 121 | icon: , 122 | }, 123 | ] 124 | 125 | return ( 126 |
127 |
128 |
129 | 130 | {'Logseq'}/ 131 | 132 | 133 | 136 |
137 | 138 |
141 | 142 | {rightActive ? 143 | : 144 | } 145 | 146 | 147 |
148 | 152 | 153 | {/*Downloads select*/} 154 |
155 | 156 | {({ active, rightIconFn, leftIconFn }: any) => { 157 | 158 | return ( 159 | 167 | ) 168 | }} 169 | 170 |
171 |
172 |
173 |
174 |
175 | ) 176 | } 177 | -------------------------------------------------------------------------------- /assets/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply text-gray-100 bg-logseq-800; 7 | 8 | &.is-nav-open { 9 | overflow: hidden; 10 | } 11 | } 12 | 13 | .app-container { 14 | @apply w-full flex-1 overflow-hidden; 15 | } 16 | 17 | .ani-slide-in-from-bottom { 18 | /* @apply animate-in fade-in-0 duration-1000 slide-in-from-bottom-14; */ 19 | @apply animate-in fade-in-0 slide-in-from-bottom-14; 20 | animation-duration: 2s; 21 | 22 | visibility: visible !important; 23 | } 24 | 25 | .ani-slide-in-from-top { 26 | @apply animate-in fade-in-0 duration-1000 slide-in-from-top-14; 27 | 28 | visibility: visible !important; 29 | } 30 | 31 | .ani-slide-in-from-left { 32 | @apply animate-in fade-in-0 duration-1000 slide-in-from-left-14; 33 | 34 | visibility: visible !important; 35 | } 36 | 37 | .ani-slide-in-from-right { 38 | @apply animate-in fade-in-0 duration-1000 slide-in-from-right-14; 39 | 40 | visibility: visible !important; 41 | } 42 | 43 | .ani-zoom-in { 44 | @apply animate-in zoom-in duration-1000 fade-in-0; 45 | 46 | visibility: visible !important; 47 | } 48 | 49 | .ani-fade-in { 50 | @apply animate-in fade-in duration-1000; 51 | 52 | visibility: visible !important; 53 | } 54 | 55 | .glass-card { 56 | @apply bg-gradient-to-b from-white/20 to-logseq-800/5 rounded-md; 57 | 58 | visibility: hidden; 59 | 60 | &-inner { 61 | @apply bg-logseq-700/50 rounded-md p-3 w-full; 62 | 63 | margin: .5px; 64 | padding-top: 15px; 65 | } 66 | } 67 | 68 | .glass-btn { 69 | @apply backdrop-blur-sm bg-logseq-500/20 px-2 py-2 rounded-md border border-gray-500/30 70 | select-none active:opacity-70 cursor-pointer hover:border-gray-500/50 flex justify-center items-center; 71 | } 72 | 73 | .app-logo-link { 74 | display: inline-block; 75 | width: 32px; 76 | height: 32px; 77 | position: relative; 78 | right: -5px; 79 | top: 1px; 80 | 81 | border-radius: 8px; 82 | box-shadow: 0 4px 32px 0 rgba(133, 200, 200, 0.3); 83 | } 84 | 85 | .app-headbar { 86 | @apply w-full px-6 fixed bg-logseq-800/70 z-50; 87 | backdrop-filter: blur(70px); 88 | 89 | .links-group { 90 | @apply flex flex-1 space-x-8 whitespace-nowrap; 91 | 92 | a { 93 | @apply text-logseq-100 sm:text-[#A4B5B6] text-base sm:text-lg hover:text-gray-300; 94 | 95 | &.app-link-active { 96 | @apply text-gray-300 font-semibold sm:opacity-100; 97 | 98 | > * { 99 | @apply opacity-100; 100 | } 101 | } 102 | } 103 | } 104 | 105 | .right-group { 106 | &-inner { 107 | @apply invisible absolute left-0 top-[54px] w-full 108 | flex-col space-x-0 items-center px-2 pb-6 opacity-0 transition-opacity duration-300 109 | sm:flex sm:flex-row sm:top-0 sm:sticky sm:opacity-100 sm:visible 110 | sm:px-0 sm:space-x-6 sm:pb-0 sm:translate-y-0; 111 | 112 | z-index: -1; 113 | 114 | @screen sm { 115 | z-index: 1; 116 | } 117 | } 118 | 119 | &:before { 120 | @apply sm:hidden z-40; 121 | 122 | display: none; 123 | content: " "; 124 | height: 100vh; 125 | width: 100vw; 126 | background: rgba(7, 7, 7, 0.43); 127 | position: fixed; 128 | left: 0; 129 | top: 90px; 130 | } 131 | 132 | &.is-active { 133 | .right-group-inner { 134 | @apply visible flex opacity-100 z-40 bg-logseq-800 sm:bg-logseq-800/0; 135 | } 136 | 137 | &:before { 138 | @apply block sm:hidden; 139 | /* display: block; */ 140 | } 141 | } 142 | } 143 | } 144 | 145 | .app-link-active { 146 | } 147 | 148 | .app-toaster { 149 | @apply !bg-logseq-500 !text-gray-100; 150 | } 151 | 152 | .app-logo { 153 | @apply flex items-center justify-center 154 | bg-logseq-700 overflow-hidden border-logseq-800 155 | shadow-2xl shadow-logseq-300 p-3 rounded-3xl; 156 | 157 | img { 158 | @apply translate-y-0.5; 159 | } 160 | } 161 | 162 | .app-logo-embossed { 163 | background: linear-gradient(to right, #0E2A35, #0E2A35) content-box content-box, 164 | linear-gradient(to bottom, rgba(133, 200, 200, 0.1), rgba(133, 200, 200, 0), rgba(0, 0, 0, 0.2)) border-box, 165 | #0E2A35 border-box; 166 | padding: 2px; 167 | display: grid; 168 | place-items: center; 169 | border-radius: 16px; 170 | filter: drop-shadow(0px 4px 16px rgba(133, 200, 200, 0.2)) drop-shadow(0px 8px 6px rgba(133, 200, 200, 0.1)); 171 | transition: filter 300ms ease-out; 172 | 173 | } 174 | 175 | .app-terms { 176 | padding-top: 80px; 177 | } 178 | 179 | .dockify-app-logo:hover .app-logo-embossed { 180 | cursor: pointer; 181 | filter: drop-shadow(0px 4px 16px rgba(133, 200, 200, 0.3)) drop-shadow(0px 8px 6px rgba(133, 200, 200, 0.2)); 182 | } 183 | 184 | .app-privacy-banner { 185 | @apply fixed left-0 bottom-0 right-0 p-6 flex justify-center 186 | z-50 bg-logseq-300/10 backdrop-blur-5xl; 187 | 188 | .r { 189 | @apply flex space-x-4 py-3 190 | justify-center sm:py-0; 191 | 192 | > button { 193 | @apply justify-center; 194 | } 195 | } 196 | } 197 | 198 | .ui-modal { 199 | @apply fixed w-screen h-screen flex justify-center 200 | top-0 left-0 z-50 bg-black/50 overflow-auto py-10; 201 | 202 | &.hidden { 203 | @apply !hidden; 204 | } 205 | 206 | &-inner { 207 | @apply relative m-auto; 208 | } 209 | 210 | &-content { 211 | @apply relative bg-logseq-700 border border-logseq-600 rounded-xl 212 | w-[90vw] max-w-[840px] min-h-[600px] p-6; 213 | } 214 | 215 | &-close { 216 | @apply absolute top-6 right-6 opacity-60 hover:opacity-80 217 | cursor-pointer active:opacity-50 select-none; 218 | } 219 | 220 | &.as-confirm { 221 | .ui-modal-content { 222 | @apply min-h-[180px] max-w-[700px]; 223 | } 224 | 225 | .ui-modal-inner { 226 | @apply top-[-50px] animate-in zoom-in duration-300; 227 | } 228 | } 229 | 230 | &-container-locked { 231 | overflow-y: hidden; 232 | } 233 | } 234 | 235 | .ui-dropdown { 236 | @apply relative; 237 | 238 | .sub-items { 239 | transform-origin: top; 240 | transform: scaleY(0); 241 | min-width: 270px; 242 | display: flex; 243 | 244 | &-inner { 245 | @apply border bg-logseq-600 rounded-md overflow-hidden 246 | border-logseq-400 p-1; 247 | 248 | > a { 249 | @apply select-none cursor-pointer rounded-md 250 | pl-3 pr-2 py-1.5 hover:bg-logseq-400/80 transition-all; 251 | } 252 | } 253 | } 254 | 255 | &.is-active { 256 | .sub-items { 257 | transform: scaleY(1); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/pages/Landing/HeadShowcase.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowSquareOut, 3 | CircleWavyQuestion, 4 | Play, 5 | } from '@phosphor-icons/react' 6 | import { Button } from '../../components/Buttons' 7 | import { GlassCard } from '../../components/Cards' 8 | import { WrapGlobalDownloadButton } from '../Downloads' 9 | import { useAppState } from '../../state' 10 | import cx from 'classnames' 11 | import { useCallback, useState } from 'react' 12 | import { openLiveDemo } from '../../components/utils' 13 | 14 | export function GlassCardRefs (props: any) { 15 | const { className, onRefMouseEnter, onRefMouseLeave, ...rest } = props 16 | return ( 17 | 18 |
19 |
20 |
21 | 22 | My notes on 📖Book / Intertwingled: 27 | 28 |
29 |
30 |
31 |
32 | ) 33 | } 34 | 35 | export function GlassCardProfile (props: any) { 36 | const { className, onRefMouseEnter, onRefMouseLeave, ...rest } = props 37 | return ( 38 | 42 |
43 | Image 44 |
45 |
46 | Jessica 48 |

👥 Person

49 |

👤 Jessica Albert

50 |
51 |
52 | ) 53 | } 54 | 55 | export function GlassCardBook (props: any) { 56 | const { className, onRefMouseEnter, onRefMouseLeave, ...rest } = props 57 | return ( 58 | 59 |
60 | Image 61 |
62 |
63 | Intertwingled 65 |

📖 Book

66 |

👤 Peter Morville

67 |
68 |
69 | ) 70 | } 71 | 72 | export function GlassCardTodo (props: any) { 73 | const { className, onRefMouseEnter, onRefMouseLeave, ...rest } = props 74 | return ( 75 | 76 |
77 |
78 |
79 | NOW 80 | 81 | Meeting with 👥 Jessica 87 | 88 |
89 | 90 | {/* children */} 91 |
92 |
93 |
94 | 95 | She mentioned her current read: 📖 Book/Intertwingled 101 | 102 |
103 |
104 |
105 |
106 |
107 |
108 | ) 109 | } 110 | 111 | export function HeadShowcase () { 112 | const appState = useAppState() 113 | const [activeRef, setActiveRef] = useState('') 114 | 115 | const refMouseEnterHandler = useCallback((e: MouseEvent) => { 116 | const target = e.target as HTMLAnchorElement 117 | setActiveRef(target.dataset.ref || '') 118 | }, []) 119 | 120 | const refMouseLeaveHandler = useCallback((e: MouseEvent) => { 121 | setActiveRef('') 122 | }, []) 123 | 124 | return ( 125 |
127 |
128 |
129 | {/* text layer*/} 130 |
131 | Connect your notes, 132 | 133 | increase understanding. 134 | 136 | 137 | 138 | 139 |
140 | 141 | {/* image layer */} 142 |
143 |
144 |
145 | 146 | {/* cards layer */} 147 |
148 |
149 | 153 |
154 | 155 |
156 | 160 | {!appState.sm.get() ? : null} 161 |
162 | 163 | {appState.sm.get() ? 164 |
165 | 166 |
: null 167 | } 168 | 169 |
170 | 174 |
175 |
176 | 177 | {/* action buttons */} 178 |
179 | 182 | {({ active, leftIconFn, rightIconFn }: any) => { 183 | const leftIcon = leftIconFn?.({ weight: 'bold', size: 18 }) 184 | const rightIcon = rightIconFn?.({ size: 18 }) 185 | 186 | return ( 187 | 193 | ) 194 | }} 195 | 196 | 197 | 205 |
206 |
207 |
208 |
209 | ) 210 | } 211 | -------------------------------------------------------------------------------- /src/pages/Pro/modals.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowSquareOut, 3 | CaretLeft, 4 | Check, CheckSquareOffset, CopySimple, 5 | IdentificationBadge, 6 | Info, PaperPlaneTilt, 7 | Student, 8 | UserCirclePlus 9 | } from '@phosphor-icons/react' 10 | import copy from 'copy-to-clipboard' 11 | import { Dropdown } from '../../components/Dropdown' 12 | import toast from 'react-hot-toast' 13 | import { useNavigate } from 'react-router-dom' 14 | import { useAppState } from '../../state' 15 | 16 | export const discountStudentEmailTemplate = { 17 | subject: 'Logseq Pro Student Discount Request', 18 | body: ` 19 | Instructions from the Logseq Team: 20 | 21 | Thank you for wanting to use Logseq Pro! Students and educational professionals can apply for a 50% discount on the monthly or annual Logseq Pro plan. 22 | To receive a discount coupon, please follow the steps below and answer the questions: 23 | Attach a photo/scan of your school ID. Please hide any personal information, but clearly show both your name and the expiry/validity date of the ID. 24 | 25 | 1. What is your full name? 26 | 2. What is your school's name? 27 | 3. What is your Logseq username? 28 | 4. What is your email address (the one you used to sign up for Logseq Pro)? 29 | 30 | Please answer all questions and attach a photo/scan of your school ID. 31 | Talk to you soon! 32 | 33 | — The Logseq team 34 | ` 35 | } 36 | 37 | export function UnlockStudentDiscount (props: any) { 38 | const { close } = props || {} 39 | const navigate = useNavigate() 40 | const appState = useAppState() 41 | const supportEmail = 'support@logseq.com' 42 | const isLogged = !!appState.value.userInfo?.username 43 | 44 | return ( 45 |
46 |

47 | Unlock your student discount. 48 |

49 | 50 |

51 | Maximize your academic potential with 50% off Logseq Pro. 52 | If you’re
currently studying at a high school or university, 53 | here’s how you can take advantage: 54 |

55 | 56 |
    57 |
  • 58 | 1 59 |
    60 |

    Create a Logseq account

    61 | 62 | Start by setting up your Logseq account at 63 | { 65 | close() 66 | navigate('/login') 67 | }} 68 | >https://logseq.com/login. 69 | 70 | 71 |
    73 | 74 | Using a university email during this step speeds up your student discount approval. 75 |
    76 | 77 | 99 |
    100 |
  • 101 | 102 |
  • 103 | 2 104 |
    105 |

    Request your discount

    106 |
    107 |
    108 |

    Already used a school email address for sign-up?

    109 |

    Simply email us directly from that school/university email address to request your discount. Make 110 | sure to also mention the username you chose for your Logseq account.

    111 | 112 | 113 | 121 | 122 |
    123 | 124 |
    125 |

    Signed up with a non-educational email address?

    126 |

    Email us a clear picture or scan of your valid student ID or other proof of student status. Mention 127 | both the email address and username you used for your Logseq account.

    128 | 129 | 133 |
    134 | 135 | 136 | 137 |
    138 | 139 |
    140 | 141 | Open email client 142 | 143 | 144 | Use our pre-filled template to request your student discount 145 | 146 |
    147 |
    148 | ), 149 | props: { 150 | href: `mailto://${supportEmail}?subject=${encodeURIComponent(discountStudentEmailTemplate.subject)}&body=${encodeURIComponent(discountStudentEmailTemplate.body)}` 151 | } 152 | }, 153 | { 154 | text: ( 155 |
    156 |
    157 | 158 | 159 | 160 |
    161 | 162 |
    163 | 164 | Copy support email address 165 | 166 | 167 | Paste it into your preferred email platform and send your request 168 | 169 |
    170 |
    171 | ), 172 | props: { 173 | onClick () { 174 | copy(supportEmail) 175 | toast('copied!') 176 | } 177 | } 178 | }]} 179 | > 180 | 187 | 188 |
    189 |
190 |
191 |

Please note:

192 |
    193 |
  • 194 | Ensure your ID displays a visible expiry date. 195 |
  • 196 |
  • 197 | Feel free to blur out any sensitive information (beside your name and the university name);
    198 | we only need to verify your student status. 199 |
  • 200 |
201 |
202 | 203 | 204 |
  • 205 | 206 |

    207 | 208 | Once your status is verified, we’ll provide you with a unique discount code. 209 |
    210 | Use this code when subscribing to Logseq Pro to get the discount. 212 |

    213 |
  • 214 | 215 | ) 216 | } 217 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", 15 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | "jsx": "preserve", 18 | /* Specify what JSX code is generated. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | 29 | /* Modules */ 30 | "module": "esnext", 31 | /* Specify what module code is generated. */ 32 | // "rootDir": "./", /* Specify the root folder within your source files. */ 33 | "moduleResolution": "node", 34 | /* Specify how TypeScript looks up a file from a given module specifier. */ 35 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 36 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 37 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 38 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 39 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 40 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 41 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 44 | 45 | /* JavaScript Support */ 46 | "allowJs": true, 47 | /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | "checkJs": false, 49 | /* Enable error reporting in type-checked JavaScript files. */ 50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 51 | 52 | /* Emit */ 53 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, 81 | /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 83 | "forceConsistentCasingInFileNames": true, 84 | /* Ensure that casing is correct in imports. */ 85 | 86 | /* Type Checking */ 87 | "strict": true, 88 | /* Enable all strict type-checking options. */ 89 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 90 | "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ 91 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 92 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 93 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 94 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 95 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 96 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 97 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 98 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 99 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 100 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 101 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 102 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 103 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 104 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 105 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 106 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 107 | 108 | /* Completeness */ 109 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 110 | "skipLibCheck": true 111 | /* Skip type checking all .d.ts files. */ 112 | }, 113 | "include": [ 114 | "src/**/*", 115 | "index.ts", 116 | "node_modules/@cloudflare/workers-types/**/*.ts" 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /src/pages/Landing/DailyShowcase.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState, useCallback } from 'react' 2 | import cx from 'classnames' 3 | import { ArrowSquareOut, CaretDown, DiscordLogo, TwitterLogo } from '@phosphor-icons/react' 4 | import { AnimateInTurnStage } from '../../components/Animations' 5 | import { useAppState } from '../../state' 6 | import { slugify, navigateTabs } from '../../components/utils' 7 | 8 | const IconImageRelations = new URL('assets/icon_relations.png', import.meta.url) 9 | const IconImageDailyPlan = new URL('assets/icon_daily_plan.png', import.meta.url) 10 | const IconImageJournals = new URL('assets/icon_journals.png', import.meta.url) 11 | const IconImageDataControl = new URL('assets/icon_data_control.png', import.meta.url) 12 | 13 | const showcases = [ 14 | { 15 | label: 'Relationships', 16 | iconUrl: IconImageRelations, 17 | descImgUrl: new URL('./assets/benefit-0.png', import.meta.url), 18 | refLink: 'https://discord.com/channels/725182569297215569/725188616695054356/1045646531090448436', 19 | userName: 'oskr', 20 | refType: 'discord', 21 | desc: ( 22 |

    23 | Communicate better. Stay on top of your
    relationships, conversations, and meetings.
    24 |

    ), 25 | feedback: ( 26 |

    27 | “I mostly use it for work: keeping daily notes & meeting notes. It's the best solution I've found 28 | to managing my tasks 29 | (and I only use a fraction of the features there).” 30 |

    31 | ) 32 | }, 33 | { 34 | label: 'Daily Plan', 35 | iconUrl: IconImageDailyPlan, 36 | descImgUrl: new URL('./assets/benefit-1.png', import.meta.url), 37 | refLink: 'https://discord.com/channels/725182569297215569/918889676071374889/1050520429258887320', 38 | refType: 'discord', 39 | userName: 'breadchris', 40 | desc: ( 41 |

    42 | Channel your attention, 43 | reduce stress. 44 |

    ), 45 | feedback: ( 46 |

    47 | “I used to hate taking notes. 48 | If I told my past self that I wouldn't just like taking notes, but that I would become addicted to it, I 49 | wouldn't believe it. 50 | Logseq has changed my life 🔥🔥🔥” 51 |

    52 | ) 53 | }, 54 | { 55 | label: 'Journaling', 56 | iconUrl: IconImageJournals, 57 | descImgUrl: new URL('./assets/benefit-2.png', import.meta.url), 58 | refLink: 'https://discord.com/channels/725182569297215569/766475028978991104/965786173148627044', 59 | refType: 'discord', 60 | userName: 'Kiernan', 61 | desc: ( 62 |

    63 | Understand yourself better. 64 |

    ), 65 | feedback: ( 66 |

    67 | “Before Logseq, I didn't use to write a daily journal, now I feel that it has a great value!” 68 |

    69 | ) 70 | }, 71 | { 72 | label: 'Data Control', 73 | iconUrl: IconImageDataControl, 74 | descImgUrl: new URL('./assets/benefit-3.png', import.meta.url), 75 | refLink: 'https://twitter.com/15777984/status/1522601138738151427', 76 | userName: '@b05crypto', 77 | desc: ( 78 |

    79 | Do all this without lock-in.
    80 | And without risking your privacy. 81 |

    ), 82 | feedback: ( 83 |

    84 | “Logseq does tracking everything in my life 85 | better than any tool I've ever used, including Roam and I have control of my data.” 86 |

    87 | ) 88 | } 89 | ] 90 | 91 | export function DailyShowcaseTabs( 92 | props: { activeShowcase: string, setActiveShowcase: any } 93 | ) { 94 | const { activeShowcase, setActiveShowcase } = props 95 | 96 | return (
    97 | {showcases.map(it => { 98 | const labelId = slugify(it.label) 99 | return ( 100 | 121 | ) 122 | })} 123 |
    ) 124 | } 125 | 126 | export function DailyShowcaseSelect( 127 | props: { activeShowcase: string, setActiveShowcase: any } 128 | ) { 129 | const { activeShowcase, setActiveShowcase } = props 130 | const activeIndex: number = showcases.findIndex(it => it.label === activeShowcase) 131 | const activeItem: any = showcases[activeIndex] 132 | 133 | return ( 134 |
    135 |
    136 | 137 | {activeItem.label} 138 | 139 | 140 | 159 | 160 | 161 | 162 | 163 |
    164 |
    165 | ) 166 | } 167 | 168 | export function DailyShowcase() { 169 | const appState = useAppState() 170 | const [showcase, setShowcase] = useState(0) 171 | const [activeShowcase, setActiveShowcase] = useState(showcases[0].label) 172 | const [sizeCache, setSizeCache] = useState([0, 0]) 173 | const [progress, setProgress] = useState(0) 174 | const bdRef = useRef(null) 175 | 176 | const nextShowcase = useCallback(() => { 177 | const total = showcases.length 178 | const currentIndex = showcases.findIndex((it) => it.label === activeShowcase) 179 | let nextIndex = currentIndex + 1 180 | if (nextIndex >= total) nextIndex = 0 181 | setActiveShowcase(showcases[nextIndex]?.label) 182 | setShowcase(nextIndex) 183 | }, [activeShowcase]) 184 | 185 | useEffect(() => { 186 | setProgress(0) 187 | }, [activeShowcase]) 188 | 189 | useEffect(() => { 190 | const timer = setInterval(() => { 191 | setProgress((progress: number) => { 192 | let nextProgress = progress + 0.4 193 | if (nextProgress > 100) { 194 | nextShowcase() 195 | nextProgress = 0 196 | } 197 | return nextProgress 198 | }) 199 | }, 60) 200 | 201 | return () => clearInterval(timer) 202 | }, [activeShowcase]) 203 | 204 | useEffect(() => { 205 | const handler = () => setSizeCache([]) 206 | window.addEventListener('resize', handler) 207 | return () => window.removeEventListener('resize', handler) 208 | }, []) 209 | 210 | return ( 211 |
    212 | 215 | {(t: Array) => { 216 | return ( 217 | <> 218 |

    221 | Logseq helps you 222 | turn this daily mess into structured information. 223 |

    224 | 225 |

    227 | Gain clarity 228 | in your everyday life: 229 |

    230 | 231 | ) 232 | }} 233 |
    234 | 235 | {/* Tabs */} 236 | {appState.sm.get() ? 237 | : 240 | } 243 | 244 | 245 | {/* Panels */} 246 |
    247 | {showcases.map(it => { 248 | if (it.label !== activeShowcase) { 249 | return null 250 | } 251 | 252 | const labelId = slugify(it.label) 253 | 254 | return ( 255 |
    256 |
    257 |
    258 | {it.desc} 259 |
    260 |
    261 | 262 |
    263 |
    269 | image { 271 | const { width, height } = e.target 272 | !sizeCache?.[0] && setSizeCache([width, height]) 273 | }} 274 | style={{width: "100%", objectFit: "cover"}} 275 | /> 276 | 277 |
    278 | {/* {*/} 280 | {/* const src = bdRef.current!.querySelector('img')?.getAttribute('src')!*/} 281 | 282 | {/* openLightbox([{ src, width: 1000, height: 596 }])*/} 283 | {/* }}*/} 284 | {/*>*/} 285 | {/* */} 290 | {/**/} 291 |
    292 |
    293 | 294 |
    295 |
    296 |
    297 |
    300 | 305 |
    306 |
    307 | 308 |
    309 |
    310 | {it.feedback} 311 |
    312 | 313 |
    314 |
    315 | User Feedback 316 | by{it.userName} 317 |
    318 |
    319 | Via 320 | {(it.refType === 'discord') ? 321 | (<> Discord) : 323 | (<> Twitter)} 325 | 327 | 328 | 329 | 330 | 331 |
    332 |
    333 |
    334 |
    335 |
    336 |
    337 |
    338 | ) 339 | })} 340 |
    341 |
    342 | ) 343 | } 344 | -------------------------------------------------------------------------------- /src/pages/Landing/LandingFooterNav.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowSquareOut, CloudCheck, DeviceMobile, FileText, 3 | GithubLogo, Globe, HandWaving, Keyhole, Play, 4 | PuzzlePiece, ScribbleLoop, Swatches, TwitterLogo, UserCirclePlus 5 | } from '@phosphor-icons/react' 6 | import { Button } from '../../components/Buttons' 7 | import { AppLogoEmbossed, FloatGlassButton, imageProductHuntLogo } from './common' 8 | import { WrapGlobalDownloadButton } from '../Downloads' 9 | import { useAppState, useModalsState } from '../../state' 10 | import { openLiveDemo } from '../../components/utils' 11 | import { Link } from 'react-router-dom' 12 | 13 | export function FooterDescCard (props: any) { 14 | const { icon, title, desc } = props 15 | return ( 16 |
    17 |
    18 |
    19 | {icon} 20 |
    21 | 22 |
    23 |

    {title}

    24 |

    {desc}

    25 |
    26 |
    27 |
    28 | ) 29 | } 30 | 31 | export function FeaturesBoards () { 32 | return ( 33 |
    34 |
    35 |
    36 |
    37 | 38 |

    Logseq Sync BETA

    39 |

    40 | Always up-to-date notes
    41 | between all your devices. 42 |

    43 |

    44 | With encrypted file syncing, you'll always have your
    notes
    46 | backed up and securely available in real-time
    on any device.
    47 |

    48 |
    49 | 50 |
    51 |
    52 |
    53 | 54 |
    55 |
    56 |
    57 | 58 |
    59 | 60 |

    Whiteboards BETA

    61 | 62 |

    63 | A new canvas
    64 | for your thoughts. 65 |

    66 | 67 |

    68 | Place any of your thoughts from the knowledge base
    69 | or new ones next to each other on an infinite canvas

    70 | to connect, associate and understand in new ways. 71 |

    72 |
    73 | 74 |
    75 |
    76 |
    77 | ) 78 | } 79 | 80 | export function FeaturesBoardsDL () { 81 | const appState = useAppState() 82 | 83 | const itemWhiteboard = ( 84 |
    85 |
    86 |
    87 | 88 |

    Whiteboards BETA

    89 | 90 | {appState.sm.get() ? 91 | ( 92 |

    93 | A new canvas
    94 | for your thoughts. 95 |

    96 | ) : ( 97 |

    98 | A new canvas 99 | for your
    thoughts.
    100 |

    101 | )} 102 |
    103 | 104 |
    105 |
    106 | ) 107 | 108 | return ( 109 | 151 | ) 152 | } 153 | 154 | export function LandingFooterDesc (props: { 155 | downloadsPage?: boolean, 156 | hideFeaturesSection?: boolean 157 | }) { 158 | const appState = useAppState() 159 | 160 | return ( 161 |
    162 | {!props.hideFeaturesSection && 163 | (
    164 | {props.downloadsPage ? 165 | <> 166 |

    168 | Get ready for
    169 | knowledge work reimagined. 170 |

    171 | 172 |

    173 | 174 | By downloading Logseq, you are embarking on a journey. We are 175 | constantly trying to make it even more useful for all kinds of 176 | workflows. 177 | 178 | {' '} 179 | 180 | These exciting features are coming soon: 181 | 182 |

    183 | 184 | : 185 | <> 186 |

    188 | A safe space for 189 | your thoughts. 190 |

    191 | 192 |

    193 | Designed to store your 194 | interests, questions, ideas, favorite quotes, 195 | reminders, reading and meeting 196 | notes 197 | easily and future-proof: 198 |

    199 | } 200 |
    )} 201 | 202 | {/* descriptions */} 203 | {!props.hideFeaturesSection && 204 | (props.downloadsPage ? 205 | null : 206 |
    207 | {[ 208 | [, 'Open source', 'Free forever for personal use'], 209 | [, 'Privacy first', 'You own your data locally forever'], 210 | [, 'Mobile apps', 'Available for iOS & Android'], 211 | [, 'Markdown files', 'Open your notes in other tools'], 212 | [, 'Strong community', ( 213 | 214 | 215 | {appState.discord?.approximate_presence_count.get() || '-'} users online currently 216 | )], 217 | [, 'Localization', 'Translated in many languages'], 218 | [, '150+ Plugins', 'Extend functionality to your needs'], 219 | [, '30+ Themes', 'Customize look and feel'], 220 | ].map(([icon, title, desc]) => { 221 | if (typeof desc === 'string') { 222 | desc = ({desc}) 223 | } 224 | return ( 225 | 226 | ) 227 | })} 228 |
    )} 229 | 230 | {/* features */} 231 | { 232 | !props.hideFeaturesSection && 233 | (props.downloadsPage ? : ) 234 | } 235 | 236 | {/* downloads */} 237 |
    238 |

    239 | Think faster, 240 | think better! 241 |

    242 | 243 |

    244 | By thinking and writing with Logseq, you'll
    245 | gain confidence in what you know and
    246 | stop worrying about forgetting
    247 | anything. 248 |

    249 | 250 |
    251 | 254 | {({ active, leftIconFn, rightIconFn }: any) => { 255 | const leftIcon = leftIconFn?.({ weight: 'bold', size: 18 }) 256 | const rightIcon = rightIconFn?.({ size: 18 }) 257 | 258 | return ( 259 | 266 | ) 267 | }} 268 | 269 | 270 | 278 |
    279 |
    280 |
    281 | ) 282 | } 283 | 284 | export function LandingFooterNav () { 285 | const appState = useAppState() 286 | 287 | const links = ( 288 |
    289 |

    290 | Privacy 291 | Terms 292 | Contact Us 293 |

    294 |

    295 | © {(new Date()).getFullYear()} Logseq, Inc. 296 |

    297 |
    298 | ) 299 | 300 | return ( 301 |
    302 |
    303 |
    304 | 305 | 306 | {appState.sm.get() ? null : links} 307 |
    308 | 309 |
    310 | 316 | 317 | 318 | 319 | 325 | 326 | 327 |
    328 |
    329 | 330 |
    331 | 342 |
    343 | 344 | {appState.sm.get() ? links : null} 345 |
    346 | ) 347 | } 348 | -------------------------------------------------------------------------------- /src/pages/Landing/TutorialTips.tsx: -------------------------------------------------------------------------------- 1 | import { AppLogoEmbossed, FloatGlassButton, openLightbox } from './common' 2 | import { 3 | ArrowCircleLeft, 4 | ArrowCircleRight, ChatsCircle, DiscordLogo, 5 | FrameCorners, MonitorPlay, Notebook, SignOut, 6 | StarFour, SignIn 7 | } from '@phosphor-icons/react' 8 | import { Button } from '../../components/Buttons' 9 | import { AnimateInTurnStage } from '../../components/Animations' 10 | import cx from 'classnames' 11 | import Swiper from 'swiper' 12 | import 'swiper/swiper-bundle.css' 13 | 14 | import 'photoswipe/dist/photoswipe.css' 15 | import { ReactNode, useEffect, useRef, useState } from 'react' 16 | import { useMounted } from '../../hooks' 17 | import { promiseImages } from './index' 18 | import { useAppState } from '../../state' 19 | 20 | export function TipSlideItem (props: { 21 | isSm: boolean, 22 | inActive: boolean, 23 | headEmoji: string, 24 | headTitle: string, 25 | content: ReactNode | string, 26 | tips: Array, 27 | complete?: () => void, 28 | activeTipChanged?: (tag: string) => void 29 | className?: string 30 | }) { 31 | const { 32 | inActive, 33 | headEmoji, 34 | headTitle, 35 | content, 36 | className, 37 | tips, 38 | complete, 39 | activeTipChanged, 40 | isSm, 41 | ...rest 42 | } = props 43 | const [activeTip, setActiveTip] = useState({ active: 0, progress: 0 }) 44 | const [_tipProgressTimer, setProgressTimer] = useState(0) // interval timer 45 | const isMounted = useMounted() 46 | 47 | const resetState = () => { 48 | setActiveTip({ active: 0, progress: 0 }) 49 | setProgressTimer(0) 50 | } 51 | 52 | const updateActiveNum = (active: number) => { 53 | setActiveTip(() => { 54 | return { active, progress: 0 } 55 | }) 56 | } 57 | 58 | useEffect(() => { 59 | if (!isMounted.current) return 60 | 61 | if (!inActive) { 62 | clear() 63 | resetState() 64 | return 65 | } 66 | 67 | function clear () { 68 | setProgressTimer((timer) => { 69 | clearInterval(timer) 70 | return 0 71 | }) 72 | } 73 | 74 | const tickHandler = () => { 75 | // async 76 | setActiveTip(({ active, progress }) => { 77 | let toProgress = progress + 0.4 78 | let toActive = active 79 | 80 | if (toProgress > 100) { 81 | if (active) { 82 | clear() 83 | complete?.() 84 | toProgress = 100 85 | } else { 86 | toProgress = 0 87 | toActive = 1 88 | } 89 | } 90 | 91 | return { active: toActive, progress: toProgress } 92 | }) 93 | } 94 | 95 | function run () { 96 | clear() 97 | const timer = setInterval(tickHandler, 60) 98 | setProgressTimer(timer as any) 99 | } 100 | 101 | console.log('run ....') 102 | run() 103 | 104 | return clear 105 | }, [inActive]) 106 | 107 | useEffect(() => { 108 | inActive && activeTipChanged?.(activeTip.active.toString()) 109 | }, [activeTip.active, inActive]) 110 | 111 | return ( 112 |
    113 | {/* Beginner */} 114 |

    115 | {headEmoji} 116 | 122 |

    123 | 124 |

    125 | {content} 126 |

    127 | 128 | 129 | updateActiveNum(0)} 130 | >1 133 | updateActiveNum(1)} 134 | >2 138 | 139 | 140 |

    141 | {tips.map((it, idx) => { 142 | if (activeTip.active !== idx) return 143 | 144 | return ( 145 | 146 | Tip {idx + 1}: 147 | {it} 148 | 149 | ) 150 | })} 151 |

    152 |
    153 | ) 154 | } 155 | 156 | export function TutorialTips () { 157 | const appState = useAppState() 158 | const swiperElRef = useRef(null) 159 | const swiperRef = useRef(null) 160 | const bdRef = useRef(null) 161 | const [activeIndex, setActiveIndex] = useState(0) 162 | const [activeTipTag, setActiveTipTag] = useState('00') 163 | const sidesLen = 3 164 | const isSm = appState.sm.get() 165 | 166 | useEffect(() => { 167 | if (swiperRef.current) return 168 | 169 | // @ts-ignore 170 | const sw = swiperRef.current = new Swiper( 171 | swiperElRef.current!, { 172 | loop: false, 173 | }, 174 | ) 175 | 176 | sw.on('activeIndexChange', () => { 177 | setActiveIndex(sw.realIndex) 178 | }) 179 | }, []) 180 | 181 | return ( 182 |
    183 | 186 | {(t: Array) => (<> 187 |

    189 | 190 | 191 | Braindump everything into 192 | 193 | 194 | 195 | 196 | . 197 | 198 | 199 | 200 | New ideas will pop up with time. 201 | 202 |

    203 | 204 | {appState.sm.get() ? 205 | null : 206 |

    207 | New ideas will pop up with time. 208 |

    209 | } 210 | 211 | 212 |

    213 | Using Logseq helps you organize your thoughts and ideas 214 |

    215 | 216 |

    217 | so that you can 218 | 219 | come up with new outputs more easily. 220 | 221 |

    222 | ) 223 | } 224 |
    225 | 226 |
    227 |
    228 |
    229 |
    230 | {/* 1 */} 231 | Get in the habit of writing {isSm ? null :
    }thoughts down every day.} 237 | tips={[ 238 | 'Think in sections, use indentation.', 239 | 'Use links & hashtags.', 240 | ]} 241 | complete={() => { 242 | swiperRef.current?.slideNext() 243 | }} 244 | activeTipChanged={(tag) => { 245 | setActiveTipTag?.(`0${tag}`) 246 | }} 247 | 248 | /> 249 | 250 | {/* 2 */} 251 | Always find what you’re {isSm ? null :
    } looking for.} 257 | tips={[ 258 | 'Use CMD-K to search with ease.', 259 | Go through linked references to find valuable information nuggets from the past., 260 | ]} 261 | complete={() => { 262 | swiperRef.current?.slideNext() 263 | }} 264 | activeTipChanged={(tag) => { 265 | setActiveTipTag?.(`1${tag}`) 266 | }} 267 | /> 268 | 269 | {/* 3 */} 270 | Create your own processes.} 276 | tips={[ 277 | Use queries to generate tables of {isSm ? null :
    } relevant information.
    , 278 | 'Install plugins and customize the app around your workflow needs.', 279 | ]} 280 | complete={() => { 281 | swiperRef.current?.slideTo(0) 282 | }} 283 | activeTipChanged={(tag) => { 284 | setActiveTipTag?.(`2${tag}`) 285 | }} 286 | /> 287 |
    288 |
    289 | 290 |
    291 | { 293 | if (activeIndex == 0) { 294 | return swiperRef.current?.slideTo(2) 295 | } 296 | 297 | swiperRef.current?.slidePrev() 298 | }} 299 | > 300 | 301 | 302 | 303 |
    305 | {Array(sidesLen).fill(0).map((_, i) => { 306 | return ( 307 | { 312 | swiperRef.current?.slideTo(i) 313 | }} 314 | > 315 | ) 316 | })} 317 |
    318 | 319 | { 321 | if (activeIndex == 2) { 322 | return swiperRef.current?.slideTo(0) 323 | } 324 | 325 | swiperRef.current?.slideNext() 326 | }} 327 | > 328 | 329 | 330 |
    331 |
    332 | 333 |
    334 |
    335 | image 337 |
    338 | 339 | {/* {*/} 342 | {/* const src = bdRef.current!.querySelector('img')?.getAttribute('src')!*/} 343 | 344 | {/* openLightbox([{ src, width: 900, height: 553 }])*/} 345 | {/* }}*/} 346 | {/*>*/} 347 | {/* */} 352 | {/**/} 353 |
    354 |
    355 | 356 | {/* more resources */} 357 |
    358 |
    360 |

    A helpful community

    361 | 362 |
    363 |
    364 | 372 | 373 | 375 | 376 | {appState.discord?.approximate_presence_count.get() || '-'} 377 | users online currently 378 | 379 |
    380 | 381 |
    382 | 390 | 392 | Feature requests, bugs, discussions 393 | 394 |
    395 |
    396 | 397 |
    398 |
    399 |
    400 | ) 401 | } 402 | -------------------------------------------------------------------------------- /src/pages/Landing/TutorialShowcase.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ClockCounterClockwise, 3 | Brain, 4 | PencilLine, 5 | CaretDown, 6 | Binoculars, 7 | Books, 8 | CalendarCheck, 9 | Lightbulb, 10 | CheckCircle, 11 | Gauge, 12 | Browsers 13 | } from '@phosphor-icons/react' 14 | import { AnimateInTurnStage } from '../../components/Animations' 15 | import cx from 'classnames' 16 | import { useState } from 'react' 17 | import { useAppState } from '../../state' 18 | import { slugify, navigateTabs } from '../../components/utils' 19 | 20 | const featuresSlideItems = [ 21 | { 22 | label: 'Students', 23 | icon: '🧑‍🎓', 24 | notes: [ 25 | { 26 | icon: , 27 | title: 'Review notes', 28 | desc: Capture, structure, and review all of your class notes with ease using the 29 | Linked References, Queries, and Search features. 30 | }, 31 | { 32 | icon: , 33 | title: 'Memorize facts', 34 | desc: (Remember facts and insights using the Flashcards feature.) 35 | }, 36 | { 37 | icon: , 38 | title: 'Outline essays', 39 | desc: ( 40 | Organize your thoughts and ideas, and quickly turn them into outlines using Block References.) 41 | } 42 | ] 43 | }, 44 | { 45 | label: 'Academics', 46 | icon: '🎓', 47 | notes: [ 48 | { 49 | icon: , 50 | title: 'Review notes', 51 | desc: Capture, structure, and review all of your class notes with ease using the 52 | Linked References, Queries, and Search features. 53 | }, 54 | { 55 | icon: , 56 | title: 'Investigate claims', 57 | desc: (Spin your web of knowledge and see what evidence supports or contradicts claims.) 58 | }, 59 | { 60 | icon: , 61 | title: 'Manage sources', 62 | desc: (Keep track of your research and easily manage your sources using the built-in 63 | Zotero integration.) 64 | }, 65 | { 66 | icon: , 67 | title: 'Outline papers', 68 | desc: ( 69 | Manage your writing process and ensure that your papers are well-organized and flow smoothly.) 70 | } 71 | ] 72 | }, 73 | { 74 | label: 'Writers', 75 | icon: '🖋', 76 | notes: [ 77 | { 78 | icon: , 79 | title: 'Review notes', 80 | desc: Capture, structure, and review all of your class notes with ease using the 81 | Linked References, Queries, and Search features. 82 | }, 83 | { 84 | icon: , 85 | title: 'Manage writing stages', 86 | desc: ( 87 | 88 | Manage your personal writing process using Tasks, 89 | Tags, and Queries --no matter how simple or complex it is. 90 | 91 | ) 92 | }, 93 | { 94 | icon: , 95 | title: 'Combine ideas', 96 | desc: ( 97 | 98 | Effortlessly synthesize ideas from across your collection of notes using 99 | Block References. 100 | 101 | ) 102 | }, 103 | { 104 | icon: , 105 | title: 'Outline content', 106 | desc: (Organize your thoughts and reuse ideas in easy-to-manage outlines.) 107 | } 108 | ] 109 | }, 110 | { 111 | label: 'Project Managers', 112 | icon: '📆', 113 | notes: [ 114 | { 115 | icon: , 116 | title: 'Retrieve notes', 117 | desc: ( 118 | Always find the information where and when you need it using 119 | Linked References, Queries or Search. 120 | ) 121 | }, 122 | { 123 | icon: , 124 | title: 'Manage tasks', 125 | desc: ( 126 | 127 | Manage tasks with Logseq's built-in 128 | Task management system, including 129 | Priorities, Scheduling, and 130 | Deadlines. 131 | 132 | ) 133 | }, 134 | { 135 | icon: , 136 | title: 'Create dashboards', 137 | desc: ( 138 | 139 | Easily create dynamic dashboards to separate tasks and notes and keep track of many projects simultaneously. 140 | 141 | ) 142 | }, 143 | { 144 | icon: , 145 | title: 'Outline documents', 146 | desc: (Quickly pull together information and share it in different formats with stakeholders.) 147 | } 148 | ] 149 | }, 150 | { 151 | label: 'Developers', 152 | icon: '💻', 153 | notes: [ 154 | { 155 | icon: , 156 | title: 'Retrieve notes', 157 | desc: ( 158 | 159 | Quickly find relevant information using the 160 | Linked References, Queries or Search. 161 | 162 | ) 163 | }, 164 | { 165 | icon: , 166 | title: 'Memorize shortcuts', 167 | desc: ( 168 | 169 | Become a 10x programmer by remembering more of what you need to do repeatedly. 170 | 171 | ) 172 | }, 173 | { 174 | icon: , 175 | title: 'Decide on solutions', 176 | desc: ( 177 | 178 | Find the best solutions to nagging problems by tracking your ideas, decisions, and priorities over time. 179 | 180 | ) 181 | }, 182 | { 183 | icon: , 184 | title: 'Build a personal wiki', 185 | desc: ( 186 | 187 | Build your career by building your personal wiki. Easily structure your knowledge and find it back 188 | using Properties, Namespaces, Linked References, and Queries. 189 | 190 | ) 191 | } 192 | ] 193 | } 194 | ] 195 | 196 | export function TutorialFeaturesDescCard ( 197 | props: typeof featuresSlideItems[number]['notes'][number] 198 | ) { 199 | return ( 200 |
    201 |

    202 | 204 | {props.icon} 205 | 206 | 207 | {props.title} 208 | 209 |

    210 | 211 |

    212 | {props.desc} 213 |

    214 |
    215 | ) 216 | } 217 | 218 | export function TutorialFeaturesPanelHolder ( 219 | props: { withoutLogo?: boolean } 220 | ) { 221 | return ( 222 |
    223 |
    224 | ) 225 | } 226 | 227 | export function TutorialFeaturesPanel ( 228 | props: Partial<{ activeItem: typeof featuresSlideItems[number] }> 229 | ) { 230 | let inner = ( 231 | <> 232 |
    233 | 234 |
    235 |
    236 | {props.activeItem?.notes.map(it => { 237 | return 238 | })} 239 |
    240 |
    241 | 242 |
    243 | 244 | ) 245 | 246 | const labelId = slugify(props.activeItem?.label) 247 | 248 | return ( 249 |
    250 | {/*
    */} 251 | {/* */} 252 | {/* 1*/} 253 | {/* 2*/} 254 | {/* */} 255 | 256 | {/*

    */} 257 | {/* Capturing and structuring class notes*/} 258 | {/*

    */} 259 | {/*
    */} 260 | 261 |
    262 |
    263 | {inner} 264 |
    265 |
    266 | 267 | {/*
    */} 268 | {/* */} 269 | {/* */} 274 | {/* */} 275 | {/*
    */} 276 |
    277 | ) 278 | } 279 | 280 | export function TutorialFeaturesSlide () { 281 | const [activeIndex, setActiveIndex] = useState(0) 282 | const activeItem = featuresSlideItems[activeIndex] 283 | 284 | return ( 285 |
    286 |
    287 | {/* Tabs */} 288 |
      289 | {featuresSlideItems.map((it, idx) => { 290 | const labelId = slugify(it.label) 291 | 292 | return ( 293 |
    • setActiveIndex(idx)} 297 | tabIndex={activeIndex === idx ? 0 : -1} 298 | role="tab" 299 | aria-controls={labelId} 300 | id={"tab-" + labelId} 301 | aria-selected={activeIndex === idx} 302 | onKeyDown={navigateTabs}> 303 |
      304 | {it.icon}{it.label} 305 |
      306 |
    • ) 307 | })} 308 |
    309 | 310 | {/* Panel */} 311 | 312 |
    313 |
    314 | ) 315 | } 316 | 317 | export function TutorialFeaturesSelect () { 318 | const [activeIndex, setActiveIndex] = useState(0) 319 | const activeItem = featuresSlideItems[activeIndex] 320 | 321 | return ( 322 |
    323 |
    324 | 325 | {activeItem.icon} 326 | 327 | 328 | 344 | 345 | 346 | 347 | 348 |
    349 | 350 | {/* Panel */} 351 | 352 |
    353 | ) 354 | } 355 | 356 | export function TutorialShowcase ( 357 | props: {}, 358 | ) { 359 | const appState = useAppState() 360 | 361 | return ( 362 |
    363 | {/* Head Slogan */} 364 | 367 | {(t: Array) => { 368 | return ( 369 | <> 370 |

    Today, 371 | everyone is 372 | a

    373 |

    knowledge 375 | worker.

    376 | 377 |
    379 |

    Logseq is the 380 | open toolbox 381 | for

    382 |

    383 | workflows that deal with lots of information: 384 |

    385 |
    386 | 387 | ) 388 | }} 389 |
    390 | 391 | {/* Head icons */} 392 |
      393 |
    • Task Management
    • 394 |
    • PDF Annotations
    • 395 |
    • Flashcards
    • 396 |
    • 397 | 398 | WhiteboardsNEW 399 | 400 |
    • 401 |
    402 | 403 | {/* Tutorial Features Slide/Select */} 404 | {appState.sm.get() ? 405 | : 406 | 407 | } 408 |
    409 | ) 410 | } 411 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { hookstate, none, StateMethods, useHookstate } from '@hookstate/core' 2 | import { IProInfo } from './types' 3 | 4 | // @ts-ignore 5 | import os from 'platform-detect/os.mjs' 6 | import { ReactElement, useEffect } from 'react' 7 | import { useAuthenticator } from '@aws-amplify/ui-react' 8 | import { Auth } from 'aws-amplify' 9 | import toast from 'react-hot-toast' 10 | import { 11 | Location, 12 | NavigateFunction, 13 | useLocation, 14 | useNavigate, 15 | } from 'react-router-dom' 16 | import camelcase from 'camelcase' 17 | import { bytesFormat, isDateValid } from './components/utils' 18 | 19 | // export const isDev = process.env.NODE_ENV !== 'production' 20 | export const isDev = false // TODO: DEBUG 21 | export const authConfig = isDev ? 22 | { 23 | region: 'us-east-2', 24 | userPoolId: 'us-east-2_kAqZcxIeM', 25 | userPoolWebClientId: '1qi1uijg8b6ra70nejvbptis0q', 26 | oauthProviders: [], 27 | } : { 28 | region: 'us-east-1', 29 | userPoolId: 'us-east-1_dtagLnju8', 30 | userPoolWebClientId: '69cs1lgme7p8kbgld8n5kseii6', 31 | oauthProviders: [] 32 | } 33 | 34 | function getAuthValueFromStorage(key: string) { 35 | const prefix = `CognitoIdentityServiceProvider.${authConfig.userPoolWebClientId}` 36 | const authUser = localStorage.getItem( 37 | `${prefix}.${authConfig.userPoolWebClientId}.LastAuthUser`) 38 | if (!authUser) return 39 | 40 | return localStorage.getItem( 41 | `${prefix}.${authConfig.userPoolWebClientId}.${authUser}.${key?.trim()}`) 42 | } 43 | 44 | export const checkSmBreakPoint = () => { 45 | return ( 46 | visualViewport?.width || 47 | document.documentElement.clientWidth 48 | ) <= 640 49 | } 50 | 51 | const defaultAppState = { 52 | isDev, os, isMobile: os.android || os.ios, 53 | sm: checkSmBreakPoint(), 54 | userInfo: { 55 | pending: false, 56 | username: null, 57 | signInUserSession: null, 58 | attributes: null, 59 | signOut: () => Promise.resolve(), 60 | }, 61 | releases: { 62 | fetching: false, 63 | downloads: {}, // macos -> download url 64 | e: false, 65 | }, 66 | discord: { 67 | guild: null, 68 | approximate_member_count: 0, 69 | approximate_presence_count: 0, 70 | }, 71 | } 72 | 73 | const appState = hookstate(defaultAppState) 74 | 75 | export type IAppState = typeof appState 76 | export type IAppUserInfoState = typeof appState.userInfo 77 | export type IAppUserInfo = typeof defaultAppState.userInfo 78 | 79 | const proState = 80 | hookstate>({}) 91 | 92 | export type IProState = typeof proState 93 | 94 | const modalsState = 95 | hookstate 101 | }> 102 | }>>({ modals: [] }) 103 | 104 | const releasesEndpoint = 'https://api.github.com/repos/logseq/logseq/releases' 105 | const discordEndpoint = 'https://discord.com/api/v9/invites/VNfUaTtdFb?with_counts=true&with_expiration=true' 106 | const fileSyncEndpoint = 'https://api-dev.logseq.com/file-sync' 107 | const logseqEndpoint = isDev 108 | ? 'https://api-dev.logseq.com/logseq' 109 | : 'https://api.logseq.com/logseq' 110 | 111 | export const lemonProductEndpoint = isDev 112 | ? 'https://logseq.lemonsqueezy.com/checkout/buy/f9a3c7cb-b8eb-42b5-b22a-7dfafad8dc09' 113 | : 'https://logseq.lemonsqueezy.com/checkout/buy/4828e0c2-e189-4102-9a90-c7101e53d525' 114 | 115 | let appliedLogin = false 116 | 117 | export function applyLoginUser( 118 | user: any, t: { 119 | navigate: NavigateFunction, 120 | routeLocation: Location, 121 | inComponent?: boolean 122 | }, 123 | ) { 124 | // for logout 125 | if (appliedLogin && !user && t.routeLocation.pathname.startsWith('/account') && 126 | t.routeLocation.pathname !== '/login') { 127 | t.navigate('/login') 128 | return 129 | } 130 | 131 | if (user?.username && user?.pool && user?.signInUserSession) { 132 | appState.userInfo.set({ 133 | signOut: async () => { 134 | console.time() 135 | appState.userInfo.pending.set(true) 136 | await Auth.signOut() 137 | appState.userInfo.pending.set(false) 138 | proState.set({}) 139 | console.timeEnd() 140 | appState.userInfo.set({} as any) 141 | }, username: user.username, 142 | signInUserSession: user.signInUserSession, 143 | attributes: user.attributes, 144 | pending: false, 145 | }) 146 | 147 | // TODO: debug 148 | if (!t.inComponent) { 149 | toast.success( 150 | `Hi, ${user.username} !`, { 151 | position: 'top-center', 152 | }) 153 | } 154 | 155 | if (t.routeLocation.pathname === '/login') { 156 | t.navigate('/account', { replace: true }) 157 | } 158 | } 159 | 160 | appliedLogin = true 161 | } 162 | 163 | export function useFetchAPI>(attachedState?: T) { 164 | const userInfo = useAppState().userInfo.get() 165 | 166 | async function loadAPI( 167 | type: string, 168 | setState: StateMethods | boolean = false, 169 | opts: Partial<{ init: RequestInit, notShowErrMsg: boolean }> = {} 170 | ) { 171 | try { 172 | // @ts-ignore 173 | const idToken = userInfo.signInUserSession?.idToken?.jwtToken 174 | const res = await fetch(`${logseqEndpoint}/${type}`, 175 | Object.assign(opts.init || {}, { 176 | method: 'POST', 177 | headers: { 178 | Authorization: `Bearer ${idToken}`, ...(opts.init?.headers || {}), 179 | }, 180 | })) 181 | 182 | const json = await res.json() 183 | 184 | if (setState) { 185 | if (setState === true) { 186 | attachedState?.[camelcase(type)].set(json) 187 | } else { 188 | setState?.[camelcase(type)].set(json) 189 | } 190 | } 191 | 192 | return json 193 | } catch (e: any) { 194 | !opts.notShowErrMsg && toast.error(e.message) 195 | return e 196 | } 197 | } 198 | 199 | return { 200 | loadAPI 201 | } 202 | } 203 | 204 | export function useAuthUserInfoState() { 205 | const { user }: any = useAuthenticator(({ user }) => [user]) 206 | const routeLocation = useLocation() 207 | const navigate = useNavigate() 208 | const { proState, loadProInfo } = useProState() 209 | const userInfoValue = appState.userInfo.get() 210 | 211 | // @ts-ignore 212 | const idToken = userInfoValue.signInUserSession?.idToken?.jwtToken 213 | 214 | useEffect(() => { 215 | if (!idToken) { 216 | proState.set({}) 217 | } else if (!proState.get().info) { 218 | loadProInfo().catch(null) 219 | } 220 | }, [idToken]) 221 | 222 | useEffect(() => { 223 | applyLoginUser(user, { navigate, routeLocation }) 224 | }, [user?.username]) 225 | } 226 | 227 | export function useReleasesState() { 228 | const state = useAppState() 229 | 230 | useEffect(() => { 231 | if (!state.releases.fetching.value) { 232 | state.releases.fetching.set(true) 233 | 234 | fetch(releasesEndpoint).then(res => res.json()).then((json) => { 235 | // TODO: parse downloads 236 | let latestRelease = null 237 | 238 | if (json && Array.isArray(json)) { 239 | json.some(it => { 240 | if (it.hasOwnProperty('tag_name') && 241 | it.tag_name?.toLowerCase() !== 'nightly' && 242 | Array.isArray(it.assets) 243 | ) { 244 | const platformMappings = { 245 | 'macos-x64': (it: string) => it.includes('x64') && 246 | it.endsWith('.dmg'), 247 | 'macos-arm64': (it: string) => it.includes('arm64') && 248 | it.endsWith('.dmg'), 249 | 'android': (it: string) => it.endsWith('.apk'), 250 | 'linux': (it: string) => it.endsWith('.AppImage'), 251 | 'windows': (it: string) => it.endsWith('.exe'), 252 | } 253 | 254 | latestRelease = it.assets.reduce((a: any, b: any) => { 255 | Object.entries(platformMappings).some(([label, validator]) => { 256 | if (validator(b.name)) { 257 | a[label] = b 258 | return true 259 | } 260 | }) 261 | 262 | return a 263 | }, {}) 264 | 265 | return true 266 | } 267 | }) 268 | 269 | state.releases.downloads.set(latestRelease as any) 270 | } 271 | 272 | if (!latestRelease) { 273 | throw new Error('Parse latest release failed!') 274 | } 275 | }).catch(e => { 276 | state.releases.e.set(e) 277 | }).finally(() => { 278 | state.releases.fetching.set(false) 279 | }) 280 | } 281 | }, []) 282 | 283 | return state.releases 284 | } 285 | 286 | export function useDiscordState() { 287 | const state = useAppState() 288 | 289 | useEffect(() => { 290 | fetch(discordEndpoint).then(res => res.json()).then((json: any) => { 291 | if (json && json.guild) { 292 | state.discord.set(json as any) 293 | } 294 | }).catch(e => { 295 | console.debug( 296 | '[Fetch Discord Err]', e, 297 | ) 298 | }) 299 | }, []) 300 | 301 | return state.discord 302 | } 303 | 304 | export function useAppState() { 305 | return useHookstate(appState) 306 | } 307 | 308 | export function useProState() { 309 | const appState = useAppState() 310 | const userInfo = appState.value.userInfo 311 | const hookProState = useHookstate(proState) 312 | const proStateValue = hookProState.get({ noproxy: true }) 313 | const proFreeTrialEndsAt = proStateValue?.info?.FreeTrialEndsAt?.LogseqPro 314 | const inTrial = proFreeTrialEndsAt && 315 | (new Date(proFreeTrialEndsAt).getTime()) > Date.now() && 316 | (proStateValue.info?.LemonStatus?.LogseqPro !== 'active') 317 | 318 | // @ts-ignore 319 | const idToken = userInfo.signInUserSession?.idToken?.jwtToken 320 | 321 | async function loadProInfo() { 322 | try { 323 | hookProState.infoFetching?.set(true) 324 | 325 | const resp = await fetch(`${logseqEndpoint}/user_info`, 326 | { method: 'POST', headers: { Authorization: `Bearer ${idToken}` } }) 327 | 328 | if (resp.status !== 200) { 329 | throw new Error(resp.statusText) 330 | } 331 | 332 | const info: IProInfo = await resp.json() 333 | 334 | // normalize info 335 | info.FileSyncStorageLimit.currentFormatted = bytesFormat(info.FileSyncStorageLimit?.current) 336 | info.FileSyncStorageLimit.proFormatted = bytesFormat(info.FileSyncStorageLimit?.pro) 337 | info.FileSyncStorageLimit.freeFormatted = bytesFormat(info.FileSyncStorageLimit?.free) 338 | 339 | hookProState.info.set(info) 340 | } catch (e: any) { 341 | console.error('[Request ProState] ', e) 342 | hookProState.e.set(e) 343 | } finally { 344 | hookProState.infoFetching?.set(false) 345 | } 346 | } 347 | 348 | return { 349 | proState: hookProState, 350 | proStateValue, inTrial, 351 | loadProInfo, 352 | } 353 | } 354 | 355 | export function useLemonState() { 356 | const { proState } = useProState() 357 | const { loadAPI } = useFetchAPI(proState) 358 | 359 | return { 360 | getSubscriptions: () => proState.lemonListSubscriptions.get( 361 | { noproxy: true }), 362 | subscriptionsFetching: proState.subscriptionsFetching.get(), 363 | loadSubscriptions: async () => { 364 | proState.subscriptionsFetching.set(true) 365 | return loadAPI('lemon_list_subscriptions', true).finally(() => 366 | proState.subscriptionsFetching.set(false)) 367 | }, 368 | cancelSubscription: async (subId: string) => { 369 | await loadAPI( 370 | 'lemon_cancel_subscription', false, 371 | { init: { body: JSON.stringify({ 'subscription-id': subId }) } }) 372 | }, 373 | pauseSubscription: async (subId: string, resumesAt?: string) => { 374 | const body = { 'subscription-id': parseInt(subId), 'mode': 'free' } 375 | if (resumesAt) { 376 | if (!isDateValid(resumesAt)) 377 | throw new Error(`Invalid resumes at date input! #${resumesAt}`) 378 | 379 | body['resumes-at'] = new Date(resumesAt).toISOString() 380 | } 381 | await loadAPI( 382 | 'lemon_pause_subscription', false, 383 | { init: { body: JSON.stringify(body) } }, 384 | ) 385 | }, 386 | unpauseSubscription: async (subId: string) => { 387 | const body = { 'subscription-id': parseInt(subId) } 388 | await loadAPI( 389 | 'lemon_unpause_subscription', false, 390 | { init: { body: JSON.stringify(body) } }, 391 | ) 392 | }, 393 | startFreeTrial: async () => { 394 | if (proState.value?.info?.ProUser) { 395 | return 396 | } 397 | 398 | await loadAPI( 399 | 'start_free_trial', false, 400 | { init: { body: JSON.stringify({ project: 'LogseqPro' }) } }, 401 | ) 402 | }, 403 | getSubscriptionRelatedInfo: async ( 404 | subId: string, 405 | resources: Array<'order' | 'subscription-invoices'> = ['subscription-invoices'], 406 | ) => { 407 | const body = { 408 | 'subscription-id': parseInt(subId), 409 | 'related-resources': resources, 410 | } 411 | 412 | try { 413 | proState.subscriptionRelatedInfo.merge({ [subId]: { pending: true } }) 414 | 415 | await new Promise(resolve => setTimeout(resolve, 5000)) 416 | 417 | const ret = await loadAPI( 418 | 'lemon_get_subscription_related_resources', false, 419 | { init: { body: JSON.stringify(body) } }, 420 | ) 421 | 422 | for (let k in ret) { 423 | if (typeof ret[k]?.body === 'string') { 424 | try { 425 | ret[k].body = JSON.parse(ret[k].body) 426 | } catch (e) { 427 | console.error(e) 428 | } 429 | } 430 | } 431 | proState.subscriptionRelatedInfo.merge({ [subId]: ret }) 432 | 433 | return ret 434 | } catch (e: any) { 435 | console.error(e) 436 | proState.subscriptionRelatedInfo.merge({ [subId]: none }) 437 | } 438 | }, 439 | } 440 | } 441 | 442 | export const createModalFacade = (ms: typeof modalsState) => { 443 | const m = ({ 444 | modals: ms.modals, 445 | topmost: () => { 446 | if (!m.modals?.length) return 447 | let it: (typeof m.modals.value)[number] 448 | // @ts-ignore 449 | while (it = m.modals.get({ noproxy: true }).pop()) { 450 | if (it.visible) { 451 | return it 452 | } 453 | } 454 | }, 455 | remove: (id: number) => ms.modals.set((v) => { 456 | const i = v.findIndex((m) => m.id === id) 457 | if (i != -1) v.splice(i, 1) 458 | return v 459 | }), 460 | create: (contentFn: (destroy: () => void) => ReactElement, props?: any) => { 461 | const id = Date.now() 462 | const idx = ms.modals.length 463 | ms.modals.set((v) => { 464 | v.push( 465 | { id, visible: false, content: contentFn(() => m.remove(id)), props }) 466 | return v 467 | }) 468 | 469 | return { 470 | id, 471 | show: () => ms.modals[idx].visible.set(true), 472 | hide: () => ms.modals[idx].visible.set(false), 473 | destroy: () => m.remove(id), 474 | } 475 | }, 476 | }) 477 | return m 478 | } 479 | 480 | export const modalFacade = createModalFacade(modalsState) 481 | 482 | export function useModalsState() { 483 | const hookModalsState = useHookstate(modalsState) 484 | return createModalFacade(hookModalsState) 485 | } 486 | 487 | // @ts-ignore 488 | window.__appState = appState 489 | // @ts-ignore 490 | window.__proState = proState 491 | // @ts-ignore 492 | window.__modalFacade = modalFacade 493 | -------------------------------------------------------------------------------- /src/pages/Downloads/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import { 3 | AppleLogo, AppStoreLogo, CaretDown, DownloadSimple, GooglePlayLogo, 4 | LinuxLogo, QrCode, WindowsLogo, 5 | } from '@phosphor-icons/react' 6 | import { useEffect, useRef, useState } from 'react' 7 | import cx from 'classnames' 8 | import { Button } from '../../components/Buttons' 9 | import { LandingFooterDesc, LandingFooterNav } from '../Landing' 10 | import { useAppState } from '../../state' 11 | import { navigateTabs } from '../../components/utils' 12 | import { Dropdown } from '../../components/Dropdown' 13 | 14 | const iosImageQr: any = new URL('assets/ios_app_qr.png', import.meta.url) 15 | const intelImageIcon: any = new URL('assets/icon_intel.png', import.meta.url) 16 | const M1ImageIcon: any = new URL('assets/icon_m1.png', import.meta.url) 17 | const WindowsImageIcon: any = new URL('assets/icon_windows_64.png', 18 | import.meta.url) 19 | const LinuxImageIcon: any = new URL('assets/icon_linux.png', import.meta.url) 20 | const GooglePlayImageIcon: any = new URL('assets/icon_google_play.png', 21 | import.meta.url) 22 | const AppleImageIcon: any = new URL('assets/icon_apple.png', import.meta.url) 23 | 24 | const releaseImages: any = { 25 | 'macos': new URL('assets/p_macos.png', import.meta.url), 26 | 'windows': new URL('assets/p_windows.png', import.meta.url), 27 | 'linux': new URL('assets/p_linux.png', import.meta.url), 28 | 'ios': new URL('assets/p_ios.png', import.meta.url), 29 | 'android': new URL('assets/p_android.png', import.meta.url), 30 | } 31 | 32 | const IntelIcon = (props: any) => { 33 | const { className, ...rest } = props 34 | 35 | return ( 36 | 39 | intel 40 | 41 | ) 42 | } 43 | 44 | const M1Icon = (props: any) => { 45 | const { className, ...rest } = props 46 | 47 | return ( 48 | 51 | M1 52 | 53 | ) 54 | } 55 | 56 | const AppleIcon = (props: any) => { 57 | const { className, ...rest } = props 58 | 59 | return ( 60 | 63 | GooglePlay 64 | 65 | ) 66 | } 67 | 68 | const Windows64Icon = (props: any) => { 69 | const { className, ...rest } = props 70 | 71 | return ( 72 | 75 | Windows64 76 | 77 | ) 78 | } 79 | const LinuxIcon = (props: any) => { 80 | const { className, ...rest } = props 81 | 82 | return ( 83 | 86 | GooglePlay 87 | 88 | ) 89 | } 90 | 91 | const GooglePlayIcon = (props: any) => { 92 | const { className, ...rest } = props 93 | 94 | return ( 95 | 98 | GooglePlay 99 | 100 | ) 101 | } 102 | 103 | const releases = [ 104 | ['MacOS', (props = {}) => ], 105 | ['Windows', (props = {}) => ], 106 | ['Linux', (props = {}) => ], 107 | ['iOS', (props = {}) => ], 108 | ['Android', (props = {}) => ], 109 | ] 110 | 111 | const resolveDownloadHref = ( 112 | appState: any, 113 | item: typeof releases[number], 114 | isIOS: boolean, 115 | platform: string = '' 116 | ) => { 117 | const rollback = 118 | isIOS ? 'https://apps.apple.com/us/app/logseq/id1601013908' : 119 | `https://github.com/logseq/logseq/releases` 120 | 121 | const downloads: any = appState.releases.downloads.get() 122 | 123 | platform = platform || item?.[0].toString().toLowerCase() 124 | 125 | if (!downloads?.[platform]) { 126 | return rollback 127 | } 128 | 129 | return downloads[platform]?.browser_download_url 130 | } 131 | 132 | export function WrapGlobalDownloadButton ( 133 | props: any = {}, 134 | ) { 135 | const { className, children, ...rest } = props 136 | const appState = useAppState() 137 | const wrapElRef = useRef(null) 138 | const os = appState.get().os 139 | const [active, setActive] = useState(releases[0]) 140 | 141 | const isIOS = active?.[0] === 'iOS' 142 | const isMacOS = active?.[0] === 'MacOS' 143 | 144 | const rightIconFn = isMacOS ? ( 145 | (props: any = {}) => 146 | ) : (isIOS ? ( 147 | (props: any = {}) => ( 148 | appState.sm.get() ? 149 | : 150 | ) 151 | ) : null) 152 | 153 | useEffect(() => { 154 | releases.some((it) => { 155 | if ( 156 | os[it?.[0].toString().toLowerCase()] 157 | ) { 158 | setActive(it) 159 | return true 160 | } 161 | }) 162 | }, [os]) 163 | 164 | const subItems = isMacOS ? ( 165 | 206 | ) : (isIOS ? ( 207 | 209 | qr 210 | 211 | ) : null 212 | ) 213 | 214 | const activePlatformIcon = active?.[1] 215 | 216 | return ( 217 | 223 | 224 | {children({ 225 | active, 226 | leftIconFn: typeof activePlatformIcon === 'function' 227 | ? activePlatformIcon 228 | : () => activePlatformIcon, 229 | rightIconFn, 230 | })} 231 | 232 | 233 | ) 234 | } 235 | 236 | export function HeadDownloadLinksSelect ( 237 | props: { activeRelease: any, setActiveRelease: any } 238 | ) { 239 | const { activeRelease, setActiveRelease } = props 240 | const activeLabel = activeRelease?.[0] 241 | 242 | let activeIcon = activeRelease?.[1] 243 | 244 | if (typeof activeIcon === 'function') { 245 | activeIcon = activeIcon() 246 | } 247 | 248 | return ( 249 |
    250 |
    251 | 252 | {activeIcon} 253 | 254 | 255 | 275 | 276 | 277 | 278 | 279 |
    280 |
    281 | ) 282 | } 283 | 284 | export function HeadDownloadLinksTabs ( 285 | props: { activeRelease: any, setActiveRelease: any } 286 | ) { 287 | const { activeRelease, setActiveRelease } = props 288 | 289 | return ( 290 |
      291 | {releases.map(([label, icon]: any) => { 292 | if (typeof icon === 'function') { 293 | icon = icon() 294 | } 295 | 296 | const isActive = activeRelease[0] === label 297 | 298 | return ( 299 |
    • { 302 | setActiveRelease([label, icon]) 303 | }} 304 | key={label} 305 | tabIndex={isActive ? 0 : -1} 306 | role="tab" 307 | aria-controls={label} 308 | id={'tab-' + label} 309 | aria-selected={isActive} 310 | onKeyDown={navigateTabs} 311 | > 312 | 313 | {icon} 314 | 315 | 316 | {label} 317 | 318 |
    • 319 | ) 320 | })} 321 |
    322 | ) 323 | } 324 | 325 | export function HeadDownloadLinks () { 326 | const appState = useAppState() 327 | const os = appState.get().os 328 | const isSm = appState.sm.get() 329 | 330 | let active = releases[0] 331 | 332 | releases.some((it) => { 333 | if ( 334 | os[it?.[0].toString().toLowerCase()] 335 | ) { 336 | active = it 337 | return true 338 | } 339 | }) 340 | 341 | const [activeRelease, setActiveRelease] = useState(active) 342 | 343 | const _getDownloadHref = (platform: string) => { 344 | const isIOS = platform === 'ios' 345 | const item: any = releases.find(it => { 346 | return (it[0] as string).toLowerCase() === platform 347 | }) 348 | 349 | return resolveDownloadHref(appState, item, isIOS, platform) 350 | } 351 | 352 | const resolvePanel = function ([label, icon]: [string, any]) { 353 | const isWindows = label.toLowerCase() === 'windows' 354 | const isAndroid = label.toLowerCase() === 'android' 355 | const isLinux = label.toLowerCase() === 'linux' 356 | const isIOS = label.toLowerCase() === 'ios' 357 | 358 | icon = isWindows ? ( 359 | 360 | ) : (isAndroid ? 361 | 362 | : (isLinux ? 363 | : 364 | (isIOS ? ( 365 | 366 | ) : icon))) 367 | 368 | switch (label) { 369 | case 'iOS': 370 | return ( 371 |
    373 | 383 | 384 | 386 | qr 387 | 388 |
    389 | ) 390 | case 'MacOS': 391 | return ( 392 |
    394 |
    395 | 405 | 406 | Most common in Macs 407 | 408 |
    409 | 410 |
    411 | 421 | 422 | Macs from November 2020 and later 423 | 424 |
    425 |
    426 | ) 427 | case 'Linux': 428 | return ( 429 |
    431 | 432 | 441 | 442 | 443 | You are advised to use AppImageLauncher for proper desktop integration 445 | 446 |
    447 | ) 448 | default: 449 | return ( 450 |
    451 | 460 |
    461 | ) 462 | } 463 | } 464 | 465 | return ( 466 |
    467 |
    468 |

    469 | Download 470 | the apps. 471 |

    472 | 473 |

    475 | 476 | Collect your thoughts and get inspired. 477 | 478 | 479 | Your train-of-thought is waiting for you! 480 | 481 |

    482 |
    483 | 484 |
    485 | {/* Tabs/Select */} 486 | {appState.sm.get() ? 487 | : 491 | 495 | } 496 | 497 | {/* Panels */} 498 |
    499 | {resolvePanel(activeRelease as any)} 500 |
    501 |
    502 | 503 |
    504 |
    505 | Image 509 |
    510 |
    511 |
    512 | ) 513 | } 514 | 515 | export function DownloadsPage () { 516 | return ( 517 |
    518 |
    519 |
    520 | 521 |
    522 |
    523 | 524 |
    525 | {/* particles background */} 526 | {/*
    */} 527 | 528 |
    529 | 530 |
    531 | 532 |
    533 |
    534 | 535 |
    536 |
    537 |
    538 |
    539 | ) 540 | } 541 | --------------------------------------------------------------------------------