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