Initial commit
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "typescript", "oxc"],
|
||||||
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the Oxlint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "typescript", "oxc"],
|
||||||
|
"options": {
|
||||||
|
"typeAware": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>journalsite</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+2540
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "journalsite",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "oxlint",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@heroui/react": "^3.2.2",
|
||||||
|
"@heroui/styles": "^3.2.2",
|
||||||
|
"react": "^19.2.7",
|
||||||
|
"react-dom": "^19.2.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@gravity-ui/icons": "^2.21.0",
|
||||||
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"@types/node": "^24.13.2",
|
||||||
|
"@types/react": "^19.2.17",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
|
"oxlint": "^1.71.0",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vite": "^8.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@heroui/styles";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--text: #6b6375;
|
||||||
|
--text-h: #08060d;
|
||||||
|
--bg: #fff;
|
||||||
|
--border: #e5e4e7;
|
||||||
|
--code-bg: #f4f3ec;
|
||||||
|
--black-background: #060607;
|
||||||
|
--default-background: #111111;
|
||||||
|
--card-background: #18181B;
|
||||||
|
--border-color: #3F3F46;
|
||||||
|
--border-onHover-Color: #71717A;
|
||||||
|
--accent: #aa3bff;
|
||||||
|
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||||
|
--accent-border: rgba(170, 59, 255, 0.5);
|
||||||
|
--social-bg: rgba(244, 243, 236, 0.5);
|
||||||
|
--shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||||
|
--button-text-color-secondary: #D0A6FE;
|
||||||
|
|
||||||
|
--sans: 'Nunito', system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--heading: 'Nunito', system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--mono: ui-monospace, Consolas, monospace;
|
||||||
|
|
||||||
|
font: 18px/145% var(--sans);
|
||||||
|
letter-spacing: 0.18px;
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--default-background);
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text: #9ca3af;
|
||||||
|
--text-h: #f3f4f6;
|
||||||
|
--bg: #16171d;
|
||||||
|
--border: #2e303a;
|
||||||
|
--code-bg: #1f2028;
|
||||||
|
--accent: #c084fc;
|
||||||
|
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||||
|
--accent-border: rgba(192, 132, 252, 0.5);
|
||||||
|
--social-bg: rgba(47, 48, 58, 0.5);
|
||||||
|
--shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Button, Card, Form, Input, Label, Link } from "@heroui/react";
|
||||||
|
import './login.css'
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
onLogin?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Login({ onLogin }: LoginProps) {
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onLogin?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex min-h-dvh flex-col overflow-hidden">
|
||||||
|
<main className="container z-10 mx-auto flex flex-1 items-center justify-center">
|
||||||
|
<Card className="w-full max-w-[420px] bg-[var(--card-background)]">
|
||||||
|
<Card.Header className="flex flex-col items-center pb-2">
|
||||||
|
<Card.Title className="text-2xl font-bold font-[Nunito] text-white">Login</Card.Title>
|
||||||
|
<Card.Description>Welcome back</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Form onSubmit={onSubmit}>
|
||||||
|
<Card.Content className="w-full">
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor="username-input"
|
||||||
|
className="pointer-events-none absolute left-4 top-2 z-10 text-xs font-medium text-white/90"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="username-input"
|
||||||
|
name="username"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
type="text"
|
||||||
|
className="h-12 w-full rounded-xl bg-transparent px-4 pt-7 pb-2 text-sm text-white hover:border-[var(--border-onHover-Color)]! data-[focus-visible=true]:border-[var(--border-onHover-Color)]!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor="password-input"
|
||||||
|
className="pointer-events-none absolute left-4 top-2 z-10 text-xs font-medium text-white/90"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password-input"
|
||||||
|
name="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
type="password"
|
||||||
|
className="h-12 w-full rounded-xl bg-transparent px-4 pt-7 pb-2 text-sm text-white hover:border-[var(--border-onHover-Color)]! data-[focus-visible=true]:border-[var(--border-onHover-Color)]!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer className="flex flex-col items-center gap-3 w-full mt-2">
|
||||||
|
<Button type="submit" fullWidth>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<Link className="text-sm no-underline" href="#">
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</Card.Footer>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.base,
|
||||||
|
.framework,
|
||||||
|
.vite {
|
||||||
|
inset-inline: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base {
|
||||||
|
width: 170px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework,
|
||||||
|
.vite {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework {
|
||||||
|
z-index: 1;
|
||||||
|
top: 34px;
|
||||||
|
height: 28px;
|
||||||
|
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||||
|
scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vite {
|
||||||
|
z-index: 0;
|
||||||
|
top: 107px;
|
||||||
|
height: 26px;
|
||||||
|
width: auto;
|
||||||
|
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||||
|
scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: white;
|
||||||
|
font-family: Nunito, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border-color: #9ca3af;
|
||||||
|
background: var(--default-background);
|
||||||
|
font-family: Nunito, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
padding: 32px 20px 24px;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { StrictMode, useState } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import Journal from "./main/journal.tsx";
|
||||||
|
import Login from "./login/Login.tsx";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
|
return loggedIn ? <Journal /> : <Login onLogin={() => setLoggedIn(true)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Card, Button } from "@heroui/react";
|
||||||
|
import {Calendar} from "@gravity-ui/icons";
|
||||||
|
|
||||||
|
interface DayViewProps {
|
||||||
|
selectedDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JournalEntry {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
time: string;
|
||||||
|
excerpt: string;
|
||||||
|
mood: string;
|
||||||
|
moodColor: string;
|
||||||
|
tags: string[];
|
||||||
|
privacy: 'only-me' | 'shared';
|
||||||
|
sharedWith?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAY_NAMES = [
|
||||||
|
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const MOCK_ENTRIES: JournalEntry[] = [
|
||||||
|
/*{
|
||||||
|
id: '1',
|
||||||
|
title: 'Morning pages',
|
||||||
|
time: '8:15 AM',
|
||||||
|
excerpt: 'Woke up early and actually felt excited about the day for once. Small thing, but I noticed it.',
|
||||||
|
mood: '🙂',
|
||||||
|
moodColor: '#27AE60',
|
||||||
|
tags: ['morning routine'],
|
||||||
|
privacy: 'only-me',
|
||||||
|
},*/
|
||||||
|
];
|
||||||
|
|
||||||
|
function formatDate(date: Date): string {
|
||||||
|
return `${DAY_NAMES[date.getDay()]}, ${MONTH_NAMES[date.getMonth()]} ${date.getDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DayView({ selectedDate }: DayViewProps) {
|
||||||
|
const formatted = formatDate(selectedDate);
|
||||||
|
const entries = MOCK_ENTRIES;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-[var(--text-h)]">{formatted}</h2>
|
||||||
|
<span className="text-sm text-[var(--text)]">
|
||||||
|
{entries.length === 0 ? 'No entries' : `${entries.length} entries`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="New entry"
|
||||||
|
className="flex size-9 items-center justify-center rounded-lg bg-[var(--card-background)] text-[var(--text-h)] transition-colors hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Default empty card block*/}
|
||||||
|
<Card className="bg-[#131313] border-2 border-dashed border-[#2D2D32] rounded-3xl">
|
||||||
|
<Card.Content className="flex flex-col items-center justify-center gap-4 py-16 px-8 text-center">
|
||||||
|
|
||||||
|
<Calendar className="size-15" />
|
||||||
|
|
||||||
|
<Card.Title className="text-white text-lg font-bold">
|
||||||
|
No entries on this day yet
|
||||||
|
</Card.Title>
|
||||||
|
|
||||||
|
<Card.Description className="text-gray-400 text-lg">
|
||||||
|
Tap below to write about {formatted}.
|
||||||
|
</Card.Description>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="font-bold h-10 px-8 bg-purple-500 shadow-lg shadow-purple-500/30 text-white/100 text-xl"
|
||||||
|
>
|
||||||
|
Write now
|
||||||
|
</Button>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DayView;
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { Card, Avatar } from "@heroui/react";
|
||||||
|
|
||||||
|
export interface FriendNote {
|
||||||
|
id: string;
|
||||||
|
friendName: string;
|
||||||
|
friendInitial: string;
|
||||||
|
avatarColor: string;
|
||||||
|
timeAgo: string;
|
||||||
|
message: string;
|
||||||
|
entryTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FriendsNotesProps {
|
||||||
|
notes?: FriendNote[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOCK_NOTES: FriendNote[] = [
|
||||||
|
/*{
|
||||||
|
id: '1',
|
||||||
|
friendName: 'Mia',
|
||||||
|
friendInitial: 'M',
|
||||||
|
avatarColor: '#C0392B',
|
||||||
|
timeAgo: '2d',
|
||||||
|
message: "This was the best day, let's do it again soon",
|
||||||
|
entryTitle: 'Beach day with Mia',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
friendName: 'Sam',
|
||||||
|
friendInitial: 'S',
|
||||||
|
avatarColor: '#2980B9',
|
||||||
|
timeAgo: '5d',
|
||||||
|
message: "You've got this. Rooting for you!",
|
||||||
|
entryTitle: 'Before the interview',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
friendName: 'Alex',
|
||||||
|
friendInitial: 'A',
|
||||||
|
avatarColor: '#27AE60',
|
||||||
|
timeAgo: '1w',
|
||||||
|
message: "Proud of the progress you're making lately.",
|
||||||
|
},*/
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FriendsNotes({ notes = MOCK_NOTES }: FriendsNotesProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-[280px] shrink-0 flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-bold text-[var(--text-h)]">Friends notes</h2>
|
||||||
|
<p className="text-xs text-[var(--text)]">Encouragement from people you've shared with</p>
|
||||||
|
</div>
|
||||||
|
{notes.length === 0 ? (
|
||||||
|
<Card className="bg-[#131313] border-2 border-dashed border-[#2D2D32] rounded-3xl">
|
||||||
|
<Card.Content className="flex flex-col items-center justify-center gap-4 py-12 px-6 text-center">
|
||||||
|
<svg
|
||||||
|
className="size-10 text-[var(--text)]"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M8.625 9.75a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 01.778-.332 48.294 48.294 0 005.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Card.Title className="text-white text-base font-bold">No notes yet</Card.Title>
|
||||||
|
<Card.Description className="text-gray-400 text-sm">
|
||||||
|
Notes from friends will show up here when you share an entry with them
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{notes.map(note => (
|
||||||
|
<Card key={note.id} className="bg-[var(--card-background)]">
|
||||||
|
<Card.Header className="flex-row items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar className="size-8">
|
||||||
|
<Avatar.Fallback
|
||||||
|
className="text-xs font-bold text-white border-none"
|
||||||
|
style={{ backgroundColor: note.avatarColor }}
|
||||||
|
>
|
||||||
|
{note.friendInitial}
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar>
|
||||||
|
<Card.Title className="text-sm font-semibold text-[var(--text-h)]">
|
||||||
|
{note.friendName}
|
||||||
|
</Card.Title>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[var(--text)]">{note.timeAgo}</span>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p className="text-sm text-[var(--text)]">{note.message}</p>
|
||||||
|
{note.entryTitle && (
|
||||||
|
<p className="mt-2 text-xs text-[var(--accent)]">on "{note.entryTitle}"</p>
|
||||||
|
)}
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FriendsNotes;
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button, Calendar, Popover, PopoverDialog, PopoverArrow } from "@heroui/react";
|
||||||
|
import { parseDate } from "@internationalized/date";
|
||||||
|
import type { CalendarDate } from "@internationalized/date";
|
||||||
|
|
||||||
|
interface WeekStripProps {
|
||||||
|
selectedDate: Date;
|
||||||
|
onDateChange: (date: Date) => void;
|
||||||
|
entryDates?: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAY_NAMES = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'] as const;
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function toISO(date: Date): string {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekSunday(date: Date): Date {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setDate(d.getDate() - d.getDay());
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date: Date, n: number): Date {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setDate(d.getDate() + n);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
|
return parseDate(toISO(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromCalendarDate(cd: CalendarDate): Date {
|
||||||
|
return new Date(cd.year, cd.month - 1, cd.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeekStrip({ selectedDate, onDateChange, entryDates = new Set() }: WeekStripProps) {
|
||||||
|
const [weekStart, setWeekStart] = useState(() => getWeekSunday(selectedDate));
|
||||||
|
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWeekStart(getWeekSunday(selectedDate));
|
||||||
|
}, [selectedDate]);
|
||||||
|
|
||||||
|
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
|
||||||
|
const todayISO = toISO(new Date());
|
||||||
|
const selectedISO = toISO(selectedDate);
|
||||||
|
const monthYearLabel = `${MONTH_NAMES[weekStart.getMonth()]} ${weekStart.getFullYear()}`;
|
||||||
|
|
||||||
|
function handlePrev() {
|
||||||
|
setWeekStart(prev => addDays(prev, -7));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNext() {
|
||||||
|
setWeekStart(prev => addDays(prev, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToday() {
|
||||||
|
const now = new Date();
|
||||||
|
setWeekStart(getWeekSunday(now));
|
||||||
|
onDateChange(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCalendarChange(calDate: CalendarDate | null) {
|
||||||
|
if (!calDate) return;
|
||||||
|
const d = fromCalendarDate(calDate);
|
||||||
|
setWeekStart(getWeekSunday(d));
|
||||||
|
onDateChange(d);
|
||||||
|
setCalendarOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-center gap-4">
|
||||||
|
<div className="flex items-center gap-15">
|
||||||
|
<button
|
||||||
|
onClick={handlePrev}
|
||||||
|
aria-label="Previous week"
|
||||||
|
className="flex size-7 items-center justify-center rounded-lg text-[var(--text)] transition-colors hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M15 18l-6-6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Popover isOpen={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||||
|
<Button variant="ghost" className="w-36 justify-center px-2 font-bold text-[var(--text-h)]">
|
||||||
|
{monthYearLabel}
|
||||||
|
</Button>
|
||||||
|
<Popover.Content offset={8}>
|
||||||
|
<PopoverDialog aria-label="Pick a date">
|
||||||
|
<PopoverArrow />
|
||||||
|
<Calendar
|
||||||
|
aria-label="Pick a date"
|
||||||
|
value={toCalendarDate(selectedDate)}
|
||||||
|
onChange={handleCalendarChange}
|
||||||
|
>
|
||||||
|
<Calendar.Header>
|
||||||
|
<Calendar.Heading />
|
||||||
|
<Calendar.NavButton slot="previous" />
|
||||||
|
<Calendar.NavButton slot="next" />
|
||||||
|
</Calendar.Header>
|
||||||
|
<Calendar.Grid>
|
||||||
|
<Calendar.GridHeader>
|
||||||
|
{(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
|
||||||
|
</Calendar.GridHeader>
|
||||||
|
<Calendar.GridBody>
|
||||||
|
{(date) => <Calendar.Cell date={date} />}
|
||||||
|
</Calendar.GridBody>
|
||||||
|
</Calendar.Grid>
|
||||||
|
</Calendar>
|
||||||
|
</PopoverDialog>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
aria-label="Next week"
|
||||||
|
className="flex size-7 items-center justify-center rounded-lg text-[var(--text)] transition-colors hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M9 18l6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Button className="text-[var(--button-text-color-secondary)]" variant="outline" size="sm" onPress={handleToday}>
|
||||||
|
Today
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{weekDays.map((date) => {
|
||||||
|
const iso = toISO(date);
|
||||||
|
const isSelected = iso === selectedISO;
|
||||||
|
const isToday = iso === todayISO;
|
||||||
|
const hasEntry = entryDates.has(iso);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={iso}
|
||||||
|
onClick={() => onDateChange(date)}
|
||||||
|
className={[
|
||||||
|
'flex flex-col items-center gap-1 rounded-lg py-2 px-1 transition-colors',
|
||||||
|
isSelected
|
||||||
|
? 'bg-[var(--accent)] text-black'
|
||||||
|
: 'text-[var(--text-h)] hover:bg-white/5',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<span className={`text-[10px] font-semibold uppercase tracking-wider ${isSelected ? 'text-black/70' : 'text-[var(--text)]'}`}>
|
||||||
|
{DAY_NAMES[date.getDay()]}
|
||||||
|
</span>
|
||||||
|
<span className="text-lg font-bold leading-none">
|
||||||
|
{date.getDate()}
|
||||||
|
</span>
|
||||||
|
{(hasEntry || isToday) ? (
|
||||||
|
<span className={`size-1.5 rounded-full ${isSelected ? 'bg-black/40' : 'bg-[var(--accent)]'}`} />
|
||||||
|
) : (
|
||||||
|
<span className="size-1.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WeekStrip;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text: #9ca3af;
|
||||||
|
--text-h: #f3f4f6;
|
||||||
|
--bg: #16171d;
|
||||||
|
--border: #2e303a;
|
||||||
|
--code-bg: #1f2028;
|
||||||
|
--accent: #c084fc;
|
||||||
|
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||||
|
--accent-border: rgba(192, 132, 252, 0.5);
|
||||||
|
--social-bg: rgba(47, 48, 58, 0.5);
|
||||||
|
--shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Dropdown,
|
||||||
|
Label,
|
||||||
|
Separator,
|
||||||
|
SearchField,
|
||||||
|
RangeCalendar,
|
||||||
|
Button,
|
||||||
|
Popover,
|
||||||
|
PopoverDialog, PopoverArrow
|
||||||
|
} from "@heroui/react";
|
||||||
|
import {Calendar} from "@gravity-ui/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { WeekStrip } from "./WeekStrip";
|
||||||
|
import { FriendsNotes } from "./FriendsNotes";
|
||||||
|
import { DayView } from "./DayView";
|
||||||
|
import './journal.css'
|
||||||
|
|
||||||
|
export function Journal() {
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<Dropdown>
|
||||||
|
<Dropdown.Trigger aria-label="Open user menu" className="group flex items-center gap-3 cursor-pointer rounded-lg p-2 hover:bg-white/5">
|
||||||
|
<Avatar color="accent" className="rounded-lg">
|
||||||
|
<Avatar.Fallback className="rounded-lg">LR</Avatar.Fallback>
|
||||||
|
</Avatar>
|
||||||
|
<span className="flex flex-col">
|
||||||
|
<span className="text-sm font-bold text-[var(--text-h)]">Lena Rain</span>
|
||||||
|
<span className="text-xs text-[var(--text)]">Private journal</span>
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className="ml-2 size-4 text-[var(--text)] transition-transform duration-200 group-aria-[expanded=true]:rotate-180"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</Dropdown.Trigger>
|
||||||
|
<Dropdown.Popover className="bg-[var(--card-background)] rounded-xl">
|
||||||
|
<Dropdown.Menu
|
||||||
|
className="p-1"
|
||||||
|
onAction={(key) => console.log(key)}
|
||||||
|
>
|
||||||
|
<Dropdown.Item id="profile" textValue="Profile" className="rounded-lg text-[var(--text-h)]">
|
||||||
|
<Label className="font-medium text-white/90">Profile</Label>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item id="settings" textValue="Settings" className="rounded-lg text-[var(--text-h)]">
|
||||||
|
<Label className="font-medium text-white/90">Settings</Label>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Separator className="bg-[var(--border-color)]" />
|
||||||
|
<Dropdown.Item id="logout" textValue="Log out" variant="danger" className="rounded-lg">
|
||||||
|
<Label>Log out</Label>
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown.Popover>
|
||||||
|
</Dropdown>
|
||||||
|
<div className="container z-10 p-7 mx-auto flex flex-1 items-center justify-center gap-4">
|
||||||
|
<SearchField name="search" aria-label="Search for entries">
|
||||||
|
<SearchField.Group>
|
||||||
|
<SearchField.SearchIcon />
|
||||||
|
<SearchField.Input className="w-[280px]" placeholder="Search entries..." />
|
||||||
|
<SearchField.ClearButton />
|
||||||
|
</SearchField.Group>
|
||||||
|
</SearchField>
|
||||||
|
<div>
|
||||||
|
<Popover>
|
||||||
|
<Button variant="secondary">
|
||||||
|
<Calendar />
|
||||||
|
<span>Pick date range</span>
|
||||||
|
</Button>
|
||||||
|
<Popover.Content className="max-w-72" offset={10}>
|
||||||
|
<PopoverDialog aria-label="Date range picker">
|
||||||
|
<PopoverArrow />
|
||||||
|
<RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon">
|
||||||
|
<RangeCalendar.Header>
|
||||||
|
<RangeCalendar.Heading />
|
||||||
|
<RangeCalendar.NavButton slot="previous" />
|
||||||
|
<RangeCalendar.NavButton slot="next" />
|
||||||
|
</RangeCalendar.Header>
|
||||||
|
<RangeCalendar.Grid>
|
||||||
|
<RangeCalendar.GridHeader>
|
||||||
|
{(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}
|
||||||
|
</RangeCalendar.GridHeader>
|
||||||
|
<RangeCalendar.GridBody>
|
||||||
|
{(date) => <RangeCalendar.Cell date={date} />}
|
||||||
|
</RangeCalendar.GridBody>
|
||||||
|
</RangeCalendar.Grid>
|
||||||
|
</RangeCalendar>
|
||||||
|
</PopoverDialog>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto max-w-3xl px-4">
|
||||||
|
<WeekStrip
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
onDateChange={setSelectedDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto mt-4 flex max-w-3xl gap-4 px-4">
|
||||||
|
<FriendsNotes />
|
||||||
|
<DayView selectedDate={selectedDate} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Journal;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023", "DOM"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"allowArbitraryExtensions": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"module": "nodenext",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user