Skip to content

Commit d48e055

Browse files
changes
1 parent 32e74b0 commit d48e055

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1022
-1086
lines changed

.github/workflows/api-approval.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ on:
33
push:
44
branches:
55
- main
6-
- dev
7-
- prod
8-
- qa
6+
97

108
pull_request:
119
branches:
1210
- main
13-
- dev
14-
- prod
15-
- qa
1611

1712
jobs:
1813
lint:

.github/workflows/client-approval.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ on:
33
push:
44
branches:
55
- main
6-
- dev
7-
- prod
8-
- qa
6+
97

108
pull_request:
119
branches:
1210
- main
13-
- dev
14-
- prod
15-
- qa
1611

1712
jobs:
1813
build:
@@ -49,7 +44,7 @@ jobs:
4944

5045
- name: Test coverage Client
5146
run: npm run coverage
52-
47+
5348
- name: Build Client
5449
run: npm run build
5550

.github/workflows/client-tests.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ on:
33
push:
44
branches:
55
- main
6-
- dev
7-
- prod
8-
- qa
6+
97

108
pull_request:
119
branches:
1210
- main
13-
- dev
14-
- prod
15-
- qa
11+
1612

1713
jobs:
1814
build:

api/app/Http/Controllers/NoteController.php

Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,131 @@
22

33
namespace App\Http\Controllers;
44

5-
use Illuminate\Http\Request;
65
use App\Models\Note;
7-
use Illuminate\Pagination\CursorPaginator;
8-
6+
use App\Models\UserNote;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Auth;
99

1010
class NoteController extends Controller
1111
{
12+
/**
13+
* Return a paginated list of the authenticated user's notes
14+
* in a shape compatible with the frontend InfiniteQuery:
15+
* {
16+
* results: UserNote[],
17+
* nextPage: int|null,
18+
* hasNextPage: bool
19+
* }
20+
*/
1221
public function index(Request $request)
13-
{
14-
$query = Note::query();
22+
{
23+
$userId = Auth::id();
1524

16-
if ($request->has('tag')) {
17-
$query->where('tag_id', $request->get('tag'));
18-
}
25+
$query = UserNote::with(['note'])
26+
->where('user_id', $userId)
27+
->orderByDesc('updated_at');
28+
29+
// Optional search against the related note title/content
30+
if ($request->filled('search')) {
31+
$search = '%' . $request->string('search')->toString() . '%';
32+
$query->whereHas('note', function ($q) use ($search) {
33+
$q->where('title', 'like', $search)
34+
->orWhere('content', 'like', $search);
35+
});
36+
}
37+
38+
// Optional sorting; allow only a limited set of columns
39+
$allowedSortColumns = ['created_at', 'updated_at'];
40+
$sortBy = $request->string('sort_by')->toString();
41+
$sortDir = $request->string('sort_direction')->toString() ?: 'desc';
42+
if ($sortBy && in_array($sortBy, $allowedSortColumns)) {
43+
$query->orderBy($sortBy, in_array(strtolower($sortDir), ['asc', 'desc']) ? $sortDir : 'desc');
44+
}
45+
46+
$perPage = (int) ($request->integer('per_page') ?: 10);
47+
$page = (int) ($request->integer('page') ?: 1);
48+
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
1949

20-
if ($request->has('notebook')) {
21-
$query->where('notebook_id', '<=', $request->get('notebook'));
50+
return response()->json([
51+
'results' => $paginator->items(),
52+
'nextPage' => $paginator->currentPage() < $paginator->lastPage()
53+
? $paginator->currentPage() + 1
54+
: null,
55+
'hasNextPage' => $paginator->hasMorePages(),
56+
]);
2257
}
2358

59+
/** Create a new note and attach to current user */
60+
public function store(Request $request)
61+
{
62+
$validated = $request->validate([
63+
'title' => ['required', 'string', 'max:255'],
64+
'content' => ['nullable', 'string'],
65+
]);
2466

25-
if ($request->has('sort_by')) {
26-
$sortBy = $request->get('sort_by');
27-
$sortDirection = $request->get('sort_direction', 'asc');
67+
$note = Note::create([
68+
'title' => $validated['title'],
69+
'content' => $validated['content'] ?? '',
70+
]);
2871

29-
// Validate sort_by to prevent SQL injection
30-
$allowedSortColumns = ['title', 'updated_at', 'created_at'];
31-
if (in_array($sortBy, $allowedSortColumns)) {
32-
$query->orderBy($sortBy, $sortDirection);
33-
}
72+
$userNote = UserNote::create([
73+
'note_id' => $note->id,
74+
'user_id' => Auth::id(),
75+
'is_favorited' => false,
76+
'is_pinned' => false,
77+
'is_trashed' => false,
78+
]);
79+
80+
$userNote->load('note');
81+
return response()->json($userNote, 201);
3482
}
3583

84+
/** Update an existing note (only if it belongs to the user) */
85+
public function update(Request $request, string $id)
86+
{
87+
$userNote = UserNote::with('note')
88+
->where('id', $id)
89+
->where('user_id', Auth::id())
90+
->firstOrFail();
91+
92+
$validated = $request->validate([
93+
'title' => ['sometimes', 'string', 'max:255'],
94+
'content' => ['sometimes', 'string', 'nullable'],
95+
'is_favorited' => ['sometimes', 'boolean'],
96+
'is_pinned' => ['sometimes', 'boolean'],
97+
'is_trashed' => ['sometimes', 'boolean'],
98+
]);
99+
100+
if (array_key_exists('title', $validated) || array_key_exists('content', $validated)) {
101+
$userNote->note->fill([
102+
'title' => $validated['title'] ?? $userNote->note->title,
103+
'content' => $validated['content'] ?? $userNote->note->content,
104+
])->save();
105+
}
36106

107+
$userNote->fill($validated)->save();
108+
$userNote->load('note');
37109

38-
if ($request->has('search')) {
39-
$searchTerm = '%' . $request->get('search') . '%';
40-
$query->where('title', 'like', $searchTerm)
41-
->orWhere('content', 'like', $searchTerm);
110+
return response()->json($userNote);
42111
}
43112

113+
/** Soft delete the link and the note (if this is the last owner) */
114+
public function destroy(string $id)
115+
{
116+
$userNote = UserNote::with('note')
117+
->where('id', $id)
118+
->where('user_id', Auth::id())
119+
->firstOrFail();
44120

45-
$notes = $query->cursorPaginate(10);
46-
return response()->json($notes);
47-
}
121+
$note = $userNote->note;
122+
$userNote->delete();
123+
124+
// If no other users are linked to this note, delete the note itself
125+
$remaining = UserNote::where('note_id', $note->id)->exists();
126+
if (!$remaining) {
127+
$note->delete();
128+
}
129+
130+
return response()->json(['status' => 'deleted']);
131+
}
48132
}

client/package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@
123123
"y-websocket": "^3.0.0",
124124
"yjs": "^13.6.27",
125125
"zod": "^4.1.12",
126-
"zustand": "^5.0.8"
126+
"zustand": "^5.0.8",
127+
"zustand-persist": "^0.4.0"
127128
},
128129
"devDependencies": {
129130
"@eslint/js": "^9.38.0",

client/src/App.tsx

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,25 @@ import '@liveblocks/react-ui/styles.css';
33
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
44
import { ErrorBoundary } from 'react-error-boundary';
55
import './App.css';
6-
//import { initializeTheme } from './hooks/use-appearance.tsx';
7-
import AuthProvider from './hooks/use-auth.tsx';
86
import { TagProvider } from './hooks/use-mutate-tag.tsx';
97
import { PageProvider } from './hooks/use-page.tsx';
10-
import { SearchProvider } from './hooks/use-search-state.tsx';
118

12-
import { EditorStoreProvider } from './hooks/use-editor-store.tsx';
13-
import { ThemeProvider } from './hooks/use-theme.tsx';
149
import ErrorFallback from './pages/error.tsx';
1510
import AppRoutes from './routes/app-routes.tsx';
1611

1712

1813
export default function App() {
19-
const queryClient = new QueryClient();
14+
const queryClient = new QueryClient();
2015

21-
return (
22-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
23-
<ErrorBoundary FallbackComponent={ErrorFallback}>
24-
<QueryClientProvider client={queryClient}>
25-
<EditorStoreProvider>
26-
<AuthProvider>
27-
<PageProvider>
28-
<SearchProvider>
29-
<TagProvider>
30-
<AppRoutes />
31-
</TagProvider>
32-
</SearchProvider>
33-
</PageProvider>
34-
</AuthProvider>
35-
</EditorStoreProvider>
36-
</QueryClientProvider>
37-
</ErrorBoundary>
38-
</ThemeProvider>
39-
);
16+
return (
17+
<ErrorBoundary FallbackComponent={ErrorFallback}>
18+
<QueryClientProvider client={queryClient}>
19+
<PageProvider>
20+
<TagProvider>
21+
<AppRoutes />
22+
</TagProvider>
23+
</PageProvider>
24+
</QueryClientProvider>
25+
</ErrorBoundary>
26+
);
4027
}

client/src/components/app-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
DropdownMenuTrigger,
1717
} from './ui/dropdown-menu';
1818

19-
import { useAuth } from '../hooks/use-auth';
19+
import { useAuth } from '../stores/use-auth-store';
2020
import {
2121
NavigationMenu,
2222
NavigationMenuItem,
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { type SVGAttributes } from 'react';
22

33
export default function AppLogoIcon(props: SVGAttributes<SVGElement>) {
4-
return (
5-
<svg {...props} viewBox="0 0 40 42" xmlns="http://www.w3.org/2000/svg">
6-
<path
7-
fillRule="evenodd"
8-
clipRule="evenodd"
9-
d="M17.2 5.63325L8.6 0.855469L0 5.63325V32.1434L16.2 41.1434L32.4 32.1434V23.699L40 19.4767V9.85547L31.4 5.07769L22.8 9.85547V18.2999L17.2 21.411V5.63325ZM38 18.2999L32.4 21.411V15.2545L38 12.1434V18.2999ZM36.9409 10.4439L31.4 13.5221L25.8591 10.4439L31.4 7.36561L36.9409 10.4439ZM24.8 18.2999V12.1434L30.4 15.2545V21.411L24.8 18.2999ZM23.8 20.0323L29.3409 23.1105L16.2 30.411L10.6591 27.3328L23.8 20.0323ZM7.6 27.9212L15.2 32.1434V38.2999L2 30.9666V7.92116L7.6 11.0323V27.9212ZM8.6 9.29991L3.05913 6.22165L8.6 3.14339L14.1409 6.22165L8.6 9.29991ZM30.4 24.8101L17.2 32.1434V38.2999L30.4 30.9666V24.8101ZM9.6 11.0323L15.2 7.92117V22.5221L9.6 25.6333V11.0323Z"
10-
/>
11-
</svg>
12-
);
4+
return (
5+
<svg {...props} viewBox="0 0 40 42" xmlns="http://www.w3.org/2000/svg">
6+
<path
7+
d="M3 3h8v21l18-21h8v36h-8V18L11 39H3V3z"
8+
fill="currentColor"
9+
fillRule="nonzero"
10+
transform="scale(-1,1) translate(-40,0)"
11+
/>
12+
</svg>
13+
);
1314
}

0 commit comments

Comments
 (0)