Merge pull request #41 from StevanFreeborn/stevanfreeborn/feat/add-search

feat: add search
This commit is contained in:
Stevan Freeborn
2023-09-16 11:42:41 -05:00
committed by GitHub
38 changed files with 1028 additions and 392 deletions
+4 -2
View File
@@ -1,6 +1,8 @@
{ {
"extends": "next/core-web-vitals", "plugins": ["prettier"],
"extends": ["next/core-web-vitals", "plugin:prettier/recommended"],
"rules": { "rules": {
"no-console": "warn" "no-console": "warn",
"prettier/prettier": "error"
} }
} }
+10
View File
@@ -0,0 +1,10 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 80,
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"embeddedLanguageFormatting": "auto",
"endOfLine": "auto"
}
+1
View File
@@ -4,6 +4,7 @@
"codemirror", "codemirror",
"conve", "conve",
"doesn", "doesn",
"Dtos",
"Keymap", "Keymap",
"lezer", "lezer",
"nextjs", "nextjs",
+70 -102
View File
@@ -1,9 +1,9 @@
import { PaginationResult, paginationOptsValidator } from 'convex/server'; import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values'; import { v } from 'convex/values';
import { PostWithUserDto } from '../src/app/types'; import { PostWithUserDto } from '../src/app/types';
import { Id } from './_generated/dataModel'; import { Doc } from './_generated/dataModel.d';
import { mutation, query } from './_generated/server'; import { QueryCtx, mutation, query } from './_generated/server';
import { userQuery } from './users'; import { createUserDto, userQuery } from './users';
export const createOrUpdatePost = mutation({ export const createOrUpdatePost = mutation({
args: { args: {
@@ -28,8 +28,10 @@ export const createOrUpdatePost = mutation({
return 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER'; return 'CANNOT_POST_ON_BEHALF_OF_ANOTHER_USER';
} }
const contentText = args.content.join('\n');
if (args.id) { if (args.id) {
await ctx.db.patch(args.id, { content: args.content }); await ctx.db.patch(args.id, { content: args.content, contentText });
return; return;
} }
@@ -37,6 +39,7 @@ export const createOrUpdatePost = mutation({
userId: user._id, userId: user._id,
content: args.content, content: args.content,
parentPostId: args.parentPostId, parentPostId: args.parentPostId,
contentText,
}); });
}, },
}); });
@@ -44,12 +47,16 @@ export const createOrUpdatePost = mutation({
export const getUsersPostById = query({ export const getUsersPostById = query({
args: { userId: v.id('users'), paginationOpts: paginationOptsValidator }, args: { userId: v.id('users'), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db const posts = await ctx.db
.query('posts') .query('posts')
.withIndex('by_user_id', q => q.eq('userId', args.userId)) .withIndex('by_user_id', q => q.eq('userId', args.userId))
.filter(q => q.eq(q.field('parentPostId'), undefined)) .filter(q => q.eq(q.field('parentPostId'), undefined))
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const postDtos = posts.page.map(createPostDto);
return { ...posts, page: postDtos };
}, },
}); });
@@ -81,14 +88,8 @@ export const getPostById = query({
} }
return { return {
...post, ...createPostDto(post),
user: { user: createUserDto(user),
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
}; };
}, },
}); });
@@ -102,35 +103,10 @@ export const getRepliesByParentId = query({
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const repliesWithUserData = await Promise.all( const repliesWithUserData = await getPostsWithUsers({
replies.page.map(async reply => { ctx,
const user = await ctx.db.get(reply.userId); posts: replies.page,
});
if (user === null) {
return {
...reply,
user: {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
},
};
}
return {
...reply,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
};
})
);
return { ...replies, page: repliesWithUserData }; return { ...replies, page: repliesWithUserData };
}, },
@@ -170,35 +146,7 @@ export const getAllPostsWithUser = query({
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const postsWithUser = await Promise.all( const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
posts.page.map(async post => {
const user = await ctx.db.get(post.userId);
if (user === null) {
return {
...post,
user: {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
},
};
}
return {
...post,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
};
})
);
return { ...posts, page: postsWithUser }; return { ...posts, page: postsWithUser };
}, },
@@ -206,7 +154,7 @@ export const getAllPostsWithUser = query({
export const getAllPostsForFollowings = query({ export const getAllPostsForFollowings = query({
args: { paginationOpts: paginationOptsValidator }, args: { paginationOpts: paginationOptsValidator },
handler: async (ctx, args): Promise<PaginationResult<PostWithUserDto>> => { handler: async (ctx, args) => {
const currentUser = await ctx.auth.getUserIdentity(); const currentUser = await ctx.auth.getUserIdentity();
if (currentUser === null) { if (currentUser === null) {
@@ -239,36 +187,56 @@ export const getAllPostsForFollowings = query({
.order('desc') .order('desc')
.paginate(args.paginationOpts); .paginate(args.paginationOpts);
const postsWithUser = await Promise.all( const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
posts.page.map(async post => {
const user = await ctx.db.get(post.userId);
if (user === null) {
return {
...post,
user: {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
},
};
}
return {
...post,
user: {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
},
};
})
);
return { ...posts, page: postsWithUser }; return { ...posts, page: postsWithUser };
}, },
}); });
export const getPostsBySearchTerm = query({
args: { term: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => {
const posts = await ctx.db
.query('posts')
.withSearchIndex('search_by_content', q =>
q.search('contentText', args.term)
)
.paginate(args.paginationOpts);
const postsWithUser = await getPostsWithUsers({ ctx, posts: posts.page });
return { ...posts, page: postsWithUser };
},
});
async function getPostsWithUsers({
ctx,
posts,
}: {
ctx: QueryCtx;
posts: Doc<'posts'>[];
}) {
return await Promise.all(
posts.map(async post => {
const postDto = createPostDto(post);
const user = await ctx.db.get(post.userId);
if (user === null) {
return {
...postDto,
user: createUserDto(user),
};
}
return {
...postDto,
user: createUserDto(user),
};
})
);
}
function createPostDto(post: Doc<'posts'>) {
const { contentText, ...postDto } = post;
return postDto;
}
+12 -2
View File
@@ -34,14 +34,24 @@ export default defineSchema(
created_at: v.number(), created_at: v.number(),
updated_at: v.number(), updated_at: v.number(),
}), }),
}).index('by_clerk_id', ['clerkUser.id']), })
.index('by_clerk_id', ['clerkUser.id'])
.searchIndex('search_by_username', {
searchField: 'clerkUser.username',
filterFields: [],
}),
posts: defineTable({ posts: defineTable({
parentPostId: v.optional(v.id('posts')), parentPostId: v.optional(v.id('posts')),
userId: v.id('users'), userId: v.id('users'),
content: v.array(v.string()), content: v.array(v.string()),
contentText: v.string(),
}) })
.index('by_user_id', ['userId']) .index('by_user_id', ['userId'])
.index('by_parent_id', ['parentPostId']), .index('by_parent_id', ['parentPostId'])
.searchIndex('search_by_content', {
searchField: 'contentText',
filterFields: ['parentPostId'],
}),
likes: defineTable({ likes: defineTable({
postId: v.id('posts'), postId: v.id('posts'),
userId: v.id('users'), userId: v.id('users'),
+40 -14
View File
@@ -1,5 +1,7 @@
import { paginationOptsValidator } from 'convex/server';
import { v } from 'convex/values'; import { v } from 'convex/values';
import { UserDto } from './../src/app/types/index'; import { UserDto } from './../src/app/types/index';
import { Doc, Id } from './_generated/dataModel';
import { import {
QueryCtx, QueryCtx,
internalMutation, internalMutation,
@@ -23,13 +25,7 @@ export const getUserByClerkId = query({
return 'USER_NOT_FOUND'; return 'USER_NOT_FOUND';
} }
return { return createUserDto(user);
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
}, },
}); });
@@ -42,13 +38,7 @@ export const getUserById = query({
return 'USER_NOT_FOUND'; return 'USER_NOT_FOUND';
} }
return { return createUserDto(user);
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
}, },
}); });
@@ -121,3 +111,39 @@ export const deleteUser = internalMutation({
await ctx.db.delete(userRecord._id); await ctx.db.delete(userRecord._id);
}, },
}); });
export const getUsersBySearchTerm = query({
args: { term: v.string(), paginationOpts: paginationOptsValidator },
handler: async (ctx, args) => {
const users = await ctx.db
.query('users')
.withSearchIndex('search_by_username', q =>
q.search('clerkUser.username', args.term)
)
.paginate(args.paginationOpts);
const userDtos = users.page.map(createUserDto);
return { ...users, page: userDtos };
},
});
export function createUserDto(user: Doc<'users'> | null) {
if (user === null) {
return {
_id: '' as Id<'users'>,
_creationTime: 0,
clerkUsername: null,
clerkImageUrl: '',
clerkUserId: '',
};
}
return {
_id: user._id,
_creationTime: user._creationTime,
clerkUsername: user.clerkUser.username,
clerkImageUrl: user.clerkUser.image_url,
clerkUserId: user.clerkUser.id,
};
}
+496
View File
@@ -26,11 +26,13 @@
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "8.49.0", "eslint": "8.49.0",
"eslint-config-next": "13.4.19", "eslint-config-next": "13.4.19",
"eslint-plugin-prettier": "^5.0.0",
"next": "13.4.19", "next": "13.4.19",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nextjs-toploader": "^1.4.2", "nextjs-toploader": "^1.4.2",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"postcss": "8.4.29", "postcss": "8.4.29",
"prettier": "^3.0.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
@@ -41,6 +43,9 @@
"svix": "^1.11.0", "svix": "^1.11.0",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"typescript": "5.2.2" "typescript": "5.2.2"
},
"devDependencies": {
"eslint-config-prettier": "^9.0.0"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@@ -1320,6 +1325,25 @@
"node": ">=10.12.0" "node": ">=10.12.0"
} }
}, },
"node_modules/@pkgr/utils": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
"integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==",
"dependencies": {
"cross-spawn": "^7.0.3",
"fast-glob": "^3.3.0",
"is-glob": "^4.0.3",
"open": "^9.1.0",
"picocolors": "^1.0.0",
"tslib": "^2.6.0"
},
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/@rushstack/eslint-patch": { "node_modules/@rushstack/eslint-patch": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz",
@@ -1955,6 +1979,14 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"node_modules/big-integer": {
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -1963,6 +1995,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bplist-parser": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
"integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
"dependencies": {
"big-integer": "^1.6.44"
},
"engines": {
"node": ">= 5.10.0"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2014,6 +2057,20 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bundle-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
"integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
"dependencies": {
"run-applescript": "^5.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/busboy": { "node_modules/busboy": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -2404,6 +2461,49 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/default-browser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
"integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
"dependencies": {
"bundle-name": "^3.0.0",
"default-browser-id": "^3.0.0",
"execa": "^7.1.1",
"titleize": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-browser-id": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
"integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
"dependencies": {
"bplist-parser": "^0.2.0",
"untildify": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-properties": { "node_modules/define-properties": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
@@ -2776,6 +2876,18 @@
} }
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-import-resolver-node": { "node_modules/eslint-import-resolver-node": {
"version": "0.3.9", "version": "0.3.9",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -2936,6 +3048,34 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/eslint-plugin-prettier": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz",
"integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.8.5"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.33.2", "version": "7.33.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
@@ -3091,6 +3231,28 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/execa": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
"integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.1",
"human-signals": "^4.3.0",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^3.0.7",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": "^14.18.0 || ^16.14.0 || >=18.0.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/extend": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -3101,6 +3263,11 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -3320,6 +3487,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-symbol-description": { "node_modules/get-symbol-description": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
@@ -3595,6 +3773,14 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/human-signals": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
"integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -3822,6 +4008,20 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3875,6 +4075,23 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
"dependencies": {
"is-docker": "^3.0.0"
},
"bin": {
"is-inside-container": "cli.js"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-map": { "node_modules/is-map": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
@@ -3969,6 +4186,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-string": { "node_modules/is-string": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@@ -4042,6 +4270,31 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-wsl/node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -4499,6 +4752,11 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5073,6 +5331,17 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5277,6 +5546,31 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/npm-run-path": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
"integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
"dependencies": {
"path-key": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nprogress": { "node_modules/nprogress": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
@@ -5407,6 +5701,37 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/onetime": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"dependencies": {
"mimic-fn": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
"integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
"dependencies": {
"default-browser": "^4.0.0",
"define-lazy-prop": "^3.0.0",
"is-inside-container": "^1.0.0",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -5694,6 +6019,31 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.29.0", "version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
@@ -6125,6 +6475,102 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/run-applescript": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
"integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
"dependencies": {
"execa": "^5.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-applescript/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/run-applescript/node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"engines": {
"node": ">=10.17.0"
}
},
"node_modules/run-applescript/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-applescript/node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/run-applescript/node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/run-applescript/node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-applescript/node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"engines": {
"node": ">=6"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6247,6 +6693,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/skin-tone": { "node_modules/skin-tone": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz",
@@ -6403,6 +6854,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -6562,6 +7024,21 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0" "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/synckit": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
"integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
"dependencies": {
"@pkgr/utils": "^2.3.1",
"tslib": "^2.5.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
@@ -6630,6 +7107,17 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/titleize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
"integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/to-no-case": { "node_modules/to-no-case": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz",
@@ -6924,6 +7412,14 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
"engines": {
"node": ">=8"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+5
View File
@@ -27,11 +27,13 @@
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "8.49.0", "eslint": "8.49.0",
"eslint-config-next": "13.4.19", "eslint-config-next": "13.4.19",
"eslint-plugin-prettier": "^5.0.0",
"next": "13.4.19", "next": "13.4.19",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nextjs-toploader": "^1.4.2", "nextjs-toploader": "^1.4.2",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"postcss": "8.4.29", "postcss": "8.4.29",
"prettier": "^3.0.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
@@ -42,5 +44,8 @@
"svix": "^1.11.0", "svix": "^1.11.0",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"typescript": "5.2.2" "typescript": "5.2.2"
},
"devDependencies": {
"eslint-config-prettier": "^9.0.0"
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { UserProfile } from '@clerk/nextjs';
export default function AccountPage() { export default function AccountPage() {
return ( return (
<main className='flex flex-col items-center'> <main className="flex flex-col items-center">
<UserProfile <UserProfile
appearance={{ appearance={{
elements: { elements: {
+3 -3
View File
@@ -2,9 +2,9 @@ import FollowingPosts from '@/components/FollowingPosts';
export default function FollowingPage() { export default function FollowingPage() {
return ( return (
<main className='flex flex-col items-center flex-1'> <main className="flex flex-col items-center flex-1">
<div className='flex flex-col w-full max-w-4xl gap-4'> <div className="flex flex-col w-full max-w-4xl gap-4">
<h1 className='text-4xl font-bold'>Following</h1> <h1 className="text-4xl font-bold">Following</h1>
<FollowingPosts /> <FollowingPosts />
</div> </div>
</main> </main>
+3 -10
View File
@@ -19,11 +19,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html <html lang="en" className="w-full h-full" suppressHydrationWarning>
lang='en'
className='w-full h-full'
suppressHydrationWarning
>
<body <body
className={`w-full h-full flex flex-col bg-white text-primary-gray dark:bg-primary-gray dark:text-white ${inter.className}`} className={`w-full h-full flex flex-col bg-white text-primary-gray dark:bg-primary-gray dark:text-white ${inter.className}`}
> >
@@ -32,12 +28,9 @@ export default function RootLayout({
> >
<ConvexProvider> <ConvexProvider>
<NextThemesProvider> <NextThemesProvider>
<NextTopLoader <NextTopLoader color="#3743e5" showSpinner={false} />
color='#3743e5'
showSpinner={false}
/>
<Navbar /> <Navbar />
<div className='flex flex-col p-4 w-full h-full overflow-y-auto'> <div className="flex flex-col p-4 w-full h-full overflow-y-auto">
{children} {children}
<Footer /> <Footer />
</div> </div>
+1 -1
View File
@@ -4,7 +4,7 @@ import { SignIn } from '@clerk/nextjs';
export default function LoginPage() { export default function LoginPage() {
return ( return (
<main className='flex flex-col flex-1 items-center'> <main className="flex flex-col flex-1 items-center">
<SignIn <SignIn
appearance={{ appearance={{
elements: { elements: {
+3 -3
View File
@@ -2,9 +2,9 @@ import AllPosts from '@/components/AllPosts';
export default function Home() { export default function Home() {
return ( return (
<main className='flex flex-col items-center flex-1'> <main className="flex flex-col items-center flex-1">
<div className='flex flex-col w-full max-w-4xl gap-4'> <div className="flex flex-col w-full max-w-4xl gap-4">
<h1 className='text-4xl font-bold'>Home</h1> <h1 className="text-4xl font-bold">Home</h1>
<AllPosts /> <AllPosts />
</div> </div>
</main> </main>
+3 -3
View File
@@ -51,7 +51,7 @@ export default function EditPostPage({ params }: { params: { id: string } }) {
} }
return ( return (
<main className='flex flex-col flex-1 w-full items-center'> <main className="flex flex-col flex-1 w-full items-center">
{user.isLoaded && post !== undefined ? ( {user.isLoaded && post !== undefined ? (
<Editor <Editor
clerkUserId={user.user.id} clerkUserId={user.user.id}
@@ -59,8 +59,8 @@ export default function EditPostPage({ params }: { params: { id: string } }) {
submitAction={submitAction} submitAction={submitAction}
/> />
) : ( ) : (
<div className='flex gap-2 items-center'> <div className="flex gap-2 items-center">
<SpinningLoader className='animate-spin w-5 h-5' /> <SpinningLoader className="animate-spin w-5 h-5" />
Loading... Loading...
</div> </div>
)} )}
+11 -18
View File
@@ -68,34 +68,27 @@ export default function PostPage({
} }
return ( return (
<main className='flex flex-col flex-1 w-full items-center'> <main className="flex flex-col flex-1 w-full items-center">
<div className='flex flex-col w-full max-w-4xl gap-4'> <div className="flex flex-col w-full max-w-4xl gap-4">
{post !== undefined && user.isLoaded ? ( {post !== undefined && user.isLoaded ? (
<> <>
<div className='flex flex-col min-w-0 p-1 rounded-md border border-gray-600'> <div className="flex flex-col min-w-0 p-1 rounded-md border border-gray-600">
<Post <Post post={post} limit={false} showReply={false} />
post={post}
limit={false}
showReply={false}
/>
</div> </div>
<div className='w-full'> <div className="w-full">
<PostReplies parentId={post._id} /> <PostReplies parentId={post._id} />
</div> </div>
<div className='flex w-full h-80 gap-4'> <div className="flex w-full h-80 gap-4">
<div> <div>
<Image <Image
alt='user profile image' alt="user profile image"
src={user.user.imageUrl} src={user.user.imageUrl}
width={40} width={40}
height={40} height={40}
className='rounded-full object-cover border-4 border-primary-accent' className="rounded-full object-cover border-4 border-primary-accent"
/> />
</div> </div>
<div <div ref={replyRef} className="flex w-full">
ref={replyRef}
className='flex w-full'
>
<Editor <Editor
clerkUserId={user.user.id} clerkUserId={user.user.id}
parentPostId={post._id} parentPostId={post._id}
@@ -106,8 +99,8 @@ export default function PostPage({
</div> </div>
</> </>
) : ( ) : (
<div className='flex w-full justify-center gap-2'> <div className="flex w-full justify-center gap-2">
<SpinningLoader className='animate-spin w-5 h-5' /> Loading post... <SpinningLoader className="animate-spin w-5 h-5" /> Loading post...
</div> </div>
)} )}
</div> </div>
+4 -7
View File
@@ -43,15 +43,12 @@ export default function AddPost() {
} }
return ( return (
<main className='flex flex-col flex-1 w-full items-center'> <main className="flex flex-col flex-1 w-full items-center">
{user.isLoaded ? ( {user.isLoaded ? (
<Editor <Editor clerkUserId={user.user.id} submitAction={submitAction} />
clerkUserId={user.user.id}
submitAction={submitAction}
/>
) : ( ) : (
<div className='flex gap-2 items-center'> <div className="flex gap-2 items-center">
<SpinningLoader className='animate-spin w-5 h-5' /> <SpinningLoader className="animate-spin w-5 h-5" />
Loading... Loading...
</div> </div>
)} )}
+2 -2
View File
@@ -19,8 +19,8 @@ export default async function UserProfilePage({
} }
return ( return (
<main className='flex flex-col flex-1 w-full items-center'> <main className="flex flex-col flex-1 w-full items-center">
<div className='w-full max-w-4xl'> <div className="w-full max-w-4xl">
<UserProfile user={user} /> <UserProfile user={user} />
<UserPosts user={user} /> <UserPosts user={user} />
</div> </div>
+26
View File
@@ -0,0 +1,26 @@
import SearchResults from '@/components/SearchResults';
export default function SearchPage({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
let term = searchParams.term;
if (term === undefined) {
term = '';
}
if (Array.isArray(term)) {
term = term.length > 0 ? term[0] : '';
}
return (
<main className="flex flex-col items-center flex-1">
<div className="flex flex-col w-full max-w-4xl gap-4">
<h1 className="text-4xl font-bold">Here&apos;s what we found</h1>
<SearchResults term={term} />
</div>
</main>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() { export default function SignUpPage() {
return ( return (
<main className='flex flex-col flex-1 items-center'> <main className="flex flex-col flex-1 items-center">
<SignUp <SignUp
appearance={{ appearance={{
elements: { elements: {
+3 -1
View File
@@ -8,4 +8,6 @@ export type UserDto = {
clerkUserId: string; clerkUserId: string;
}; };
export type PostWithUserDto = Doc<'posts'> & { user: UserDto }; export type PostWithUserDto = Omit<Doc<'posts'>, 'contentText'> & {
user: UserDto;
};
+5 -5
View File
@@ -19,8 +19,8 @@ export default function AllPosts() {
if (pager.isLoading === false && pager.results.length == 0) { if (pager.isLoading === false && pager.results.length == 0) {
return ( return (
<div className='flex flex-col justify-center items-center gap-2'> <div className="flex flex-col justify-center items-center gap-2">
<h2 className='text-2xl'> <h2 className="text-2xl">
Hmmm it doesn&apos;t look like anyone has posted. Hmmm it doesn&apos;t look like anyone has posted.
</h2> </h2>
</div> </div>
@@ -28,12 +28,12 @@ export default function AllPosts() {
} }
return ( return (
<div className='flex flex-col items-center w-full'> <div className="flex flex-col items-center w-full">
<div className='flex flex-col items-center w-full'> <div className="flex flex-col items-center w-full">
{pager.results.map(post => ( {pager.results.map(post => (
<div <div
key={post._id} key={post._id}
className='w-full border border-t-0 border-gray-600 first:border-t last:rounded-b-md p-1 last:mb-4' className="w-full border border-t-0 border-gray-600 first:border-t last:rounded-b-md p-1 last:mb-4"
> >
<Post post={post} /> <Post post={post} />
</div> </div>
+12 -15
View File
@@ -92,30 +92,30 @@ export default function Editor({
return ( return (
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='flex flex-col flex-1 w-full shadow-md rounded-md dark:bg-secondary-gray border border-gray-600' className="flex flex-col flex-1 w-full shadow-md rounded-md dark:bg-secondary-gray border border-gray-600"
> >
<div className='flex items-center justify-between rounded-t-md p-4 pb-0 border-b border-gray-600 dark:bg-primary-gray'> <div className="flex items-center justify-between rounded-t-md p-4 pb-0 border-b border-gray-600 dark:bg-primary-gray">
<div className='flex items-center'> <div className="flex items-center">
<button <button
className='flex items-center justify-center px-3 py-2 rounded-t-md -mb-[1px] disabled:bg-white disabled:border disabled:border-b-0 disabled:border-gray-600 disabled:dark:bg-secondary-gray' className="flex items-center justify-center px-3 py-2 rounded-t-md -mb-[1px] disabled:bg-white disabled:border disabled:border-b-0 disabled:border-gray-600 disabled:dark:bg-secondary-gray"
disabled={mode === 'write'} disabled={mode === 'write'}
onClick={() => setMode('write')} onClick={() => setMode('write')}
type='button' type="button"
> >
Write Write
</button> </button>
<button <button
className='flex items-center justify-center px-3 py-2 rounded-t-md -mb-[1px] disabled:bg-white disabled:border disabled:border-b-0 disabled:border-gray-600 disabled:dark:bg-secondary-gray' className="flex items-center justify-center px-3 py-2 rounded-t-md -mb-[1px] disabled:bg-white disabled:border disabled:border-b-0 disabled:border-gray-600 disabled:dark:bg-secondary-gray"
disabled={mode === 'preview'} disabled={mode === 'preview'}
onClick={() => setMode('preview')} onClick={() => setMode('preview')}
type='button' type="button"
> >
Preview Preview
</button> </button>
</div> </div>
<div></div> <div></div>
</div> </div>
<div className='flex flex-col pt-4 px-4 h-0 flex-grow overflow-auto'> <div className="flex flex-col pt-4 px-4 h-0 flex-grow overflow-auto">
<div <div
className={`${mode === 'write' ? '' : 'hidden'} flex-1 overflow-auto`} className={`${mode === 'write' ? '' : 'hidden'} flex-1 overflow-auto`}
ref={editorRef} ref={editorRef}
@@ -124,21 +124,18 @@ export default function Editor({
<PostContent content={Text.of(currentDoc).toString()} /> <PostContent content={Text.of(currentDoc).toString()} />
</div> </div>
</div> </div>
<div className='flex items-center justify-end p-4 gap-4'> <div className="flex items-center justify-end p-4 gap-4">
<button <button onClick={() => router.back()} type="button">
onClick={() => router.back()}
type='button'
>
Cancel Cancel
</button> </button>
<button <button
type='submit' type="submit"
disabled={ disabled={
!currentDoc.join() || !currentDoc.join() ||
!currentDoc.join().trim() || !currentDoc.join().trim() ||
creatingOrUpdating creatingOrUpdating
} }
className='py-1 px-4 bg-primary-accent text-white rounded-md disabled:opacity-50 flex items-center justify-center gap-2' className="py-1 px-4 bg-primary-accent text-white rounded-md disabled:opacity-50 flex items-center justify-center gap-2"
> >
Post Post
</button> </button>
+7 -7
View File
@@ -20,14 +20,14 @@ export default function FollowingPosts() {
if (pager.isLoading === false && pager.results.length == 0) { if (pager.isLoading === false && pager.results.length == 0) {
return ( return (
<div className='flex flex-col justify-center items-center gap-2'> <div className="flex flex-col justify-center items-center gap-2">
<h2 className='text-2xl'> <h2 className="text-2xl">
Hmmm it doesn&apos;t look like you follow anyone or anyone who has Hmmm it doesn&apos;t look like you follow anyone or anyone who has
made a post yet. made a post yet.
</h2> </h2>
<Link <Link
href='/' href="/"
className='max-w-max py-1 px-3 text-white bg-primary-accent rounded-md' className="max-w-max py-1 px-3 text-white bg-primary-accent rounded-md"
> >
Find someone to follow Find someone to follow
</Link> </Link>
@@ -36,12 +36,12 @@ export default function FollowingPosts() {
} }
return ( return (
<div className='flex flex-col items-center w-full'> <div className="flex flex-col items-center w-full">
<div className='flex flex-col items-center w-full'> <div className="flex flex-col items-center w-full">
{pager.results.map(post => ( {pager.results.map(post => (
<div <div
key={post._id} key={post._id}
className='w-full border border-t-0 border-gray-600 first:border-t last:rounded-b-md p-1 last:mb-4' className="w-full border border-t-0 border-gray-600 first:border-t last:rounded-b-md p-1 last:mb-4"
> >
<Post post={post} /> <Post post={post} />
</div> </div>
+11 -23
View File
@@ -2,43 +2,31 @@ import Link from 'next/link';
export default function Footer() { export default function Footer() {
return ( return (
<div className='flex flex-col w-full items-center justify-center px-4 mt-4'> <div className="flex flex-col w-full items-center justify-center px-4 mt-4">
<footer className='flex flex-col items-center justify-between gap-2 w-full max-w-4xl text-sm text-primary-white border-t border-gray-600 py-4'> <footer className="flex flex-col items-center justify-between gap-2 w-full max-w-4xl text-sm text-primary-white border-t border-gray-600 py-4">
<div className='flex items-center justify-center w-full flex-wrap gap-4'> <div className="flex items-center justify-center w-full flex-wrap gap-4">
<Link <Link href="/about" className="hover:text-primary-orange">
href='/about'
className='hover:text-primary-orange'
>
About About
</Link> </Link>
<Link <Link href="/contact" className="hover:text-primary-orange">
href='/contact'
className='hover:text-primary-orange'
>
Contact Contact
</Link> </Link>
<Link <Link href="/terms" className="hover:text-primary-orange">
href='/terms'
className='hover:text-primary-orange'
>
Terms Terms
</Link> </Link>
<Link <Link href="/privacy" className="hover:text-primary-orange">
href='/privacy'
className='hover:text-primary-orange'
>
Privacy Privacy
</Link> </Link>
<Link <Link
href='https://github.com/StevanFreeborn/conve-x' href="https://github.com/StevanFreeborn/conve-x"
className='hover:text-primary-orange' className="hover:text-primary-orange"
> >
Code Code
</Link> </Link>
</div> </div>
<div> <div>
<span className='self-center whitespace-nowrap italic'> <span className="self-center whitespace-nowrap italic">
conve<span className='font-bold text-primary-accent'>X</span> conve<span className="font-bold text-primary-accent">X</span>
</span> </span>
<span> &copy; 2023</span> <span> &copy; 2023</span>
</div> </div>
+5 -5
View File
@@ -11,8 +11,8 @@ export default function Loader({
}) { }) {
if (isLoading) { if (isLoading) {
return ( return (
<div className='flex w-full items-center justify-center gap-2 p-5 border-t border-gray-600 text-sm'> <div className="flex w-full items-center justify-center gap-2 p-5 text-sm">
<SpinningLoader className='animate-spin w-5 h-5' /> <SpinningLoader className="animate-spin w-5 h-5" />
Loading... Loading...
</div> </div>
); );
@@ -23,15 +23,15 @@ export default function Loader({
} }
return ( return (
<div className='flex items-center'> <div className="flex items-center">
<button <button
className='bg-primary-accent text-white px-3 py-1 rounded-full text-sm' className="bg-primary-accent text-white px-3 py-1 rounded-full text-sm"
onClick={loadButtonClickHandler} onClick={loadButtonClickHandler}
disabled={status !== 'CanLoadMore'} disabled={status !== 'CanLoadMore'}
> >
{isLoading ? ( {isLoading ? (
<> <>
<SpinningLoader className='animate-spin w-5 h-5' /> <SpinningLoader className="animate-spin w-5 h-5" />
Loading... Loading...
</> </>
) : ( ) : (
+63 -63
View File
@@ -1,11 +1,12 @@
'use client'; 'use client';
import { useRouter } from '@/hooks';
import { SignedIn, SignedOut, useUser } from '@clerk/nextjs'; import { SignedIn, SignedOut, useUser } from '@clerk/nextjs';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { AiFillHome } from 'react-icons/ai'; import { AiFillHome } from 'react-icons/ai';
import { BiLogIn, BiPlus } from 'react-icons/bi'; import { BiLogIn, BiPlus } from 'react-icons/bi';
import { BsPersonPlusFill } from 'react-icons/bs'; import { BsPersonPlusFill, BsSearch } from 'react-icons/bs';
import { ImProfile } from 'react-icons/im'; import { ImProfile } from 'react-icons/im';
import { import {
RiMenuFoldLine, RiMenuFoldLine,
@@ -16,8 +17,10 @@ import ThemeButton from './ThemeButton';
import UserButton from './UserButton'; import UserButton from './UserButton';
export default function Navbar() { export default function Navbar() {
const [term, setTerm] = useState('');
const { user } = useUser(); const { user } = useUser();
const [navOpen, setNavOpen] = useState(false); const [navOpen, setNavOpen] = useState(false);
const router = useRouter();
useEffect(() => { useEffect(() => {
function handler() { function handler() {
@@ -33,68 +36,74 @@ export default function Navbar() {
}, []); }, []);
return ( return (
<nav className='flex flex-col items-center gap-4 p-5 shadow-md dark:bg-secondary-gray md:flex-row md:justify-between'> <nav className="flex flex-col items-center gap-4 p-5 shadow-md dark:bg-secondary-gray md:flex-row md:justify-between">
<div className='flex items-center justify-between w-full md:max-w-min'> <div className="flex items-center justify-between w-full md:max-w-min">
<Link <Link href="/" className="italic">
href='/'
className='italic'
>
conve conve
<span className='font-bold text-4xl text-primary-accent'>X</span> <span className="font-bold text-4xl text-primary-accent">X</span>
</Link> </Link>
<button <button
onClick={() => setNavOpen(!navOpen)} onClick={() => setNavOpen(!navOpen)}
type='button' type="button"
className='flex items-center justify-center md:hidden' className="flex items-center justify-center md:hidden"
> >
{navOpen ? <RiMenuUnfoldLine /> : <RiMenuFoldLine />} {navOpen ? <RiMenuUnfoldLine /> : <RiMenuFoldLine />}
</button> </button>
</div> </div>
<div <div
className='hidden flex-1 flex-col gap-4 w-full items-center md:flex md:flex-row md:justify-between' className="hidden flex-1 flex-col gap-4 w-full items-center md:flex md:flex-row md:justify-between"
style={{ display: navOpen ? 'flex' : '' }} style={{ display: navOpen ? 'flex' : '' }}
> >
<ul className='flex flex-col w-full items-start gap-4 md:flex-row md:items-center md:max-w-min'> <ul className="flex flex-col w-full items-start gap-4 md:flex-row md:items-center md:max-w-min">
<SignedIn> <SignedIn>
<li className='flex w-full'> <li className="flex w-full">
<div className='flex w-full'> <form
onSubmit={e => {
e.preventDefault();
router.push(`/search?term=${term}`);
}}
className="flex w-full relative"
>
<input <input
type='text' onChange={e => setTerm(e.target.value)}
placeholder='Search' value={term}
className='w-full md:w-[unset] py-1 px-2 rounded-md border border-gray-600 dark:bg-primary-gray dark:border-0' type="text"
placeholder="Search"
className="w-full md:w-[unset] py-1 px-2 rounded-md border border-gray-600 dark:bg-primary-gray dark:border-0"
/> />
</div> <button
className="flex items-center justify-center absolute right-0 top-1/2 transform -translate-x-1/2 -translate-y-1/2"
disabled={!term || !term.trim()}
type="submit"
>
<BsSearch />
</button>
</form>
</li> </li>
<li> <li>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='p-1 md:hidden'> <div className="p-1 md:hidden">
<AiFillHome className='w-6 h-6' /> <AiFillHome className="w-6 h-6" />
</div> </div>
<Link <Link onClick={() => setNavOpen(false)} href="/">
onClick={() => setNavOpen(false)}
href='/'
>
Home Home
</Link> </Link>
</div> </div>
</li> </li>
<li> <li>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='p-1 md:hidden'> <div className="p-1 md:hidden">
<RiUserFollowFill className='w-6 h-6' /> <RiUserFollowFill className="w-6 h-6" />
</div> </div>
<Link <Link onClick={() => setNavOpen(false)} href="/following">
onClick={() => setNavOpen(false)}
href='/following'
>
Following Following
</Link> </Link>
</div> </div>
</li> </li>
<li> <li>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='p-1 md:hidden'> <div className="p-1 md:hidden">
<ImProfile className='w-6 h-6' /> <ImProfile className="w-6 h-6" />
</div> </div>
<Link <Link
onClick={() => setNavOpen(false)} onClick={() => setNavOpen(false)}
@@ -104,55 +113,46 @@ export default function Navbar() {
</Link> </Link>
</div> </div>
</li> </li>
<li className='flex items-center gap-2 max-w-min whitespace-nowrap'> <li className="flex items-center gap-2 max-w-min whitespace-nowrap">
<div className='p-1 bg-primary-accent rounded-md rounded-bl-none'> <div className="p-1 bg-primary-accent rounded-md rounded-bl-none">
<Link <Link onClick={() => setNavOpen(false)} href="/posts/add">
onClick={() => setNavOpen(false)} <BiPlus className="w-6 h-6 text-white" />
href='/posts/add'
>
<BiPlus className='w-6 h-6 text-white' />
</Link> </Link>
</div> </div>
<span className='md:hidden'>Add Post</span> <span className="md:hidden">Add Post</span>
</li> </li>
</SignedIn> </SignedIn>
</ul> </ul>
<ul className='flex flex-col w-full items-start gap-4 md:flex-row md:items-center md:max-w-min'> <ul className="flex flex-col w-full items-start gap-4 md:flex-row md:items-center md:max-w-min">
<li className='flex items-center gap-2'> <li className="flex items-center gap-2">
<div className='p-1'> <div className="p-1">
<ThemeButton /> <ThemeButton />
</div> </div>
<span className='md:hidden'>Change Theme</span> <span className="md:hidden">Change Theme</span>
</li> </li>
<SignedIn> <SignedIn>
<li className='flex items-center gap-2'> <li className="flex items-center gap-2">
<UserButton /> <UserButton />
<span className='md:hidden'>Manage Account</span> <span className="md:hidden">Manage Account</span>
</li> </li>
</SignedIn> </SignedIn>
<SignedOut> <SignedOut>
<li> <li>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='p-1 md:hidden'> <div className="p-1 md:hidden">
<BiLogIn className='w-6 h-6' /> <BiLogIn className="w-6 h-6" />
</div> </div>
<Link <Link href="/login" className="whitespace-nowrap">
href='/login'
className='whitespace-nowrap'
>
Sign in Sign in
</Link> </Link>
</div> </div>
</li> </li>
<li> <li>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<div className='p-1 md:hidden'> <div className="p-1 md:hidden">
<BsPersonPlusFill className='w-6 h-6' /> <BsPersonPlusFill className="w-6 h-6" />
</div> </div>
<Link <Link href="/signup" className="whitespace-nowrap">
href='/signup'
className='whitespace-nowrap'
>
Sign up Sign up
</Link> </Link>
</div> </div>
+13 -22
View File
@@ -44,30 +44,27 @@ export default function Post({
} }
return ( return (
<div className='flex w-full gap-4 p-8 bg-white dark:bg-secondary-gray flex-wrap'> <div className="flex w-full gap-4 p-8 bg-white dark:bg-secondary-gray flex-wrap">
<div className='flex-shrink-0'> <div className="flex-shrink-0">
<Link href={`/profile/${post.user.clerkUserId}`}> <Link href={`/profile/${post.user.clerkUserId}`}>
<Image <Image
alt='user profile image' alt="user profile image"
src={post.user.clerkImageUrl} src={post.user.clerkImageUrl}
width={40} width={40}
height={40} height={40}
className='rounded-full object-cover border-4 border-primary-accent' className="rounded-full object-cover border-4 border-primary-accent"
/> />
</Link> </Link>
</div> </div>
<div className='flex flex-1 flex-col gap-2 min-w-0'> <div className="flex flex-1 flex-col gap-2 min-w-0">
<div className='flex items-center justify-between gap-2'> <div className="flex items-center justify-between gap-2">
<div className='flex items-center gap-2 flex-wrap min-w-0'> <div className="flex items-center gap-2 flex-wrap min-w-0">
<div <div className="overflow-hidden text-ellipsis" title={username}>
className='overflow-hidden text-ellipsis'
title={username}
>
{username} {username}
</div> </div>
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
<GoDotFill className='w-3 h-3' /> <GoDotFill className="w-3 h-3" />
<div className='text-xs'> <div className="text-xs">
{`${postMonth} ${dayOfMonth}, ${postYear}`} {`${postMonth} ${dayOfMonth}, ${postYear}`}
</div> </div>
</div> </div>
@@ -83,18 +80,12 @@ export default function Post({
</div> </div>
<div> <div>
{limit && post.content.length > 10 ? ( {limit && post.content.length > 10 ? (
<Link <Link className="font-semibold text-primary-accent" href={postLink}>
className='font-semibold text-primary-accent'
href={postLink}
>
Show more Show more
</Link> </Link>
) : null} ) : null}
</div> </div>
<PostActionButton <PostActionButton postId={post._id} showReply={showReply} />
postId={post._id}
showReply={showReply}
/>
</div> </div>
</div> </div>
); );
+6 -6
View File
@@ -29,14 +29,14 @@ export default function PostActionButton({
} }
return ( return (
<div className='flex items-center gap-4'> <div className="flex items-center gap-4">
{showReply && replyCount !== undefined ? ( {showReply && replyCount !== undefined ? (
<Link <Link
className='flex items-center justify-center gap-2 px-3 py-1 border border-gray-600 rounded-md hover:bg-primary-accent hover:text-white' className="flex items-center justify-center gap-2 px-3 py-1 border border-gray-600 rounded-md hover:bg-primary-accent hover:text-white"
href={{ pathname: `/posts/${postId}`, query: { reply: true } }} href={{ pathname: `/posts/${postId}`, query: { reply: true } }}
> >
<BsFillReplyFill /> <BsFillReplyFill />
<div className='flex items-center justify-center h-6 w-6 rounded-full text-xs bg-primary-gray text-white'> <div className="flex items-center justify-center h-6 w-6 rounded-full text-xs bg-primary-gray text-white">
{replyCount} {replyCount}
</div> </div>
</Link> </Link>
@@ -44,13 +44,13 @@ export default function PostActionButton({
{like !== undefined && likeCount !== undefined ? ( {like !== undefined && likeCount !== undefined ? (
<button <button
onClick={handleLikeClick} onClick={handleLikeClick}
type='button' type="button"
className={`flex items-center justify-center gap-2 px-3 py-1 border border-gray-600 rounded-md ${ className={`flex items-center justify-center gap-2 px-3 py-1 border border-gray-600 rounded-md ${
isLiked ? 'text-primary-accent' : '' isLiked ? 'text-primary-accent' : ''
}`} }`}
> >
<BiSolidLike className='flex-shrink-0' /> <BiSolidLike className="flex-shrink-0" />
<div className='flex items-center justify-center h-6 w-6 rounded-full text-xs bg-primary-gray text-white'> <div className="flex items-center justify-center h-6 w-6 rounded-full text-xs bg-primary-gray text-white">
{likeCount} {likeCount}
</div> </div>
</button> </button>
+3 -6
View File
@@ -36,7 +36,7 @@ export default function PostActionModal({ postId }: { postId: Id<'posts'> }) {
} }
return ( return (
<div className='relative'> <div className="relative">
<button onClick={() => setModalOpen(!modalOpen)}> <button onClick={() => setModalOpen(!modalOpen)}>
<BiDotsHorizontalRounded /> <BiDotsHorizontalRounded />
</button> </button>
@@ -55,11 +55,8 @@ export default function PostActionModal({ postId }: { postId: Id<'posts'> }) {
Edit post Edit post
</Link> </Link>
</li> </li>
<li className='text-red-600'> <li className="text-red-600">
<button <button onClick={handleDeleteButtonClick} type="button">
onClick={handleDeleteButtonClick}
type='button'
>
Delete post Delete post
</button> </button>
</li> </li>
+3 -6
View File
@@ -7,7 +7,7 @@ import './MarkdownContent.css';
export default function PostContent({ content }: { content: string }) { export default function PostContent({ content }: { content: string }) {
return ( return (
<div className='markdown-content'> <div className="markdown-content">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm, remarkEmoji]} remarkPlugins={[remarkGfm, remarkEmoji]}
components={{ components={{
@@ -19,15 +19,12 @@ export default function PostContent({ content }: { content: string }) {
{...props} {...props}
style={oneDark} style={oneDark}
language={match[1]} language={match[1]}
PreTag='div' PreTag="div"
> >
{String(children).replace(/\n$/, '')} {String(children).replace(/\n$/, '')}
</SyntaxHighlighter> </SyntaxHighlighter>
) : ( ) : (
<code <code {...props} className={className}>
{...props}
className={className}
>
{children} {children}
</code> </code>
); );
+3 -6
View File
@@ -21,13 +21,10 @@ export default function PostReplies({ parentId }: { parentId: Id<'posts'> }) {
} }
return ( return (
<div className='flex flex-col-reverse items-center w-full gap-4'> <div className="flex flex-col-reverse items-center w-full gap-4">
<div className='flex flex-col-reverse items-center w-full gap-4'> <div className="flex flex-col-reverse items-center w-full gap-4">
{pager.results.map(reply => ( {pager.results.map(reply => (
<div <div key={reply._id} className="w-full">
key={reply._id}
className='w-full'
>
<Reply post={reply} /> <Reply post={reply} />
</div> </div>
))} ))}
+12 -15
View File
@@ -16,39 +16,36 @@ export default function Reply({ post }: { post: PostWithUserDto }) {
const year = created.getFullYear(); const year = created.getFullYear();
return ( return (
<div className='flex w-full gap-4'> <div className="flex w-full gap-4">
<div> <div>
<Link href={`/profile/${post.user.clerkUserId}`}> <Link href={`/profile/${post.user.clerkUserId}`}>
<Image <Image
alt='user profile image' alt="user profile image"
src={post.user.clerkImageUrl} src={post.user.clerkImageUrl}
width={40} width={40}
height={40} height={40}
className='rounded-full object-cover border-4 border-primary-accent' className="rounded-full object-cover border-4 border-primary-accent"
/> />
</Link> </Link>
</div> </div>
<div className='flex flex-col flex-1 w-full border border-gray-600 rounded-md'> <div className="flex flex-col flex-1 w-full border border-gray-600 rounded-md">
<div className='flex flex-col w-full p-1'> <div className="flex flex-col w-full p-1">
<div className='flex justify-between dark:bg-primary-gray p-2'> <div className="flex justify-between dark:bg-primary-gray p-2">
<div className='text-sm'> <div className="text-sm">
{`${username} `} {`${username} `}
<span className='text-[#7a838f]'>{`replied on ${month} ${day}, ${year}`}</span> <span className="text-[#7a838f]">{`replied on ${month} ${day}, ${year}`}</span>
</div> </div>
{isLoaded && isSignedIn && user.id === post.user.clerkUserId ? ( {isLoaded && isSignedIn && user.id === post.user.clerkUserId ? (
<PostActionModal postId={post._id} /> <PostActionModal postId={post._id} />
) : null} ) : null}
</div> </div>
<div className='p-1'> <div className="p-1">
<div className='p-2 rounded-md dark:bg-secondary-gray'> <div className="p-2 rounded-md dark:bg-secondary-gray">
<PostContent content={Text.of(post.content).toString()} /> <PostContent content={Text.of(post.content).toString()} />
</div> </div>
</div> </div>
<div className='p-1'> <div className="p-1">
<PostActionButton <PostActionButton postId={post._id} showReply={false} />
postId={post._id}
showReply={false}
/>
</div> </div>
</div> </div>
</div> </div>
+150
View File
@@ -0,0 +1,150 @@
'use client';
import { PostWithUserDto, UserDto } from '@/app/types';
import { UsePaginatedQueryReturnType, usePaginatedQuery } from 'convex/react';
import { FunctionReference } from 'convex/server';
import { useState } from 'react';
import { api } from '../../convex/_generated/api';
import Loader from './Loader';
import Post from './Post';
import UserProfile from './UserProfile';
type Pager<T> = UsePaginatedQueryReturnType<
FunctionReference<
'query',
'public',
{
term: string;
paginationOpts: {
id?: number | undefined;
numItems: number;
cursor: string | null;
};
},
{
page: T[];
isDone: boolean;
continueCursor: string;
}
>
>;
function NoResults() {
return (
<div className="flex flex-col justify-center items-center gap-2">
<h2 className="text-xl">
Hmmm it doesn&apos;t look like we could find anything.
</h2>
</div>
);
}
function UserSearchResults({
pager,
pageSize,
}: {
pager: Pager<UserDto>;
pageSize: number;
}) {
const { status, results, isLoading, loadMore } = pager;
if (isLoading === false && results.length == 0) {
return <NoResults />;
}
return (
<div>
<div className="flex flex-col gap-2">
{results.map(user => (
<div key={user._id}>
<UserProfile user={user} />
</div>
))}
</div>
<Loader
isLoading={isLoading}
status={status}
loadButtonClickHandler={() => loadMore(pageSize)}
/>
</div>
);
}
function PostSearchResults({
pager,
pageSize,
}: {
pager: Pager<PostWithUserDto>;
pageSize: number;
}) {
const { status, results, isLoading, loadMore } = pager;
if (isLoading === false && results.length == 0) {
return <NoResults />;
}
return (
<div className="flex flex-col items-center w-full">
<div className="flex flex-col items-center w-full">
{results.map(post => (
<div
key={post._id}
className="w-full border border-t-0 border-gray-600 first:border-t last:rounded-b-md p-1 last:mb-4"
>
<Post post={post} />
</div>
))}
</div>
<Loader
isLoading={isLoading}
status={status}
loadButtonClickHandler={() => loadMore(pageSize)}
/>
</div>
);
}
export default function SearchResults({ term }: { term: string }) {
const PAGE_SIZE = 10;
const [current, setCurrent] = useState<'posts' | 'users'>('posts');
const postsPager = usePaginatedQuery(
api.posts.getPostsBySearchTerm,
{ term },
{ initialNumItems: PAGE_SIZE }
);
const usersPager = usePaginatedQuery(
api.users.getUsersBySearchTerm,
{ term },
{ initialNumItems: PAGE_SIZE }
);
return (
<div className="flex flex-col gap-4">
<div className="flex gap-2 border-b border-gray-600">
<button
onClick={() => setCurrent('posts')}
disabled={current === 'posts'}
type="button"
className="py-1 px-2 border-b-4 border-transparent disabled:border-primary-accent"
>
Posts
</button>
<button
onClick={() => setCurrent('users')}
disabled={current === 'users'}
type="button"
className="py-1 px-2 border-b-4 border-transparent disabled:border-primary-accent"
>
Users
</button>
</div>
{current === 'posts' ? (
<PostSearchResults pager={postsPager} pageSize={PAGE_SIZE} />
) : (
<UserSearchResults pager={usersPager} pageSize={PAGE_SIZE} />
)}
</div>
);
}
+4 -4
View File
@@ -13,15 +13,15 @@ export default function ThemeButton() {
return ( return (
<button <button
className='flex items-center justify-center' className="flex items-center justify-center"
onClick={() => setTheme(isDark ? 'light' : 'dark')} onClick={() => setTheme(isDark ? 'light' : 'dark')}
> >
{mounted === false ? ( {mounted === false ? (
<Loader className='animate-spin w-6 h-6' /> <Loader className="animate-spin w-6 h-6" />
) : isDark ? ( ) : isDark ? (
<BsMoonStarsFill className='w-6 h-6' /> <BsMoonStarsFill className="w-6 h-6" />
) : ( ) : (
<BsSunFill className='w-6 h-6' /> <BsSunFill className="w-6 h-6" />
)} )}
</button> </button>
); );
+4 -4
View File
@@ -19,10 +19,10 @@ export default function UserButton({ children }: { children?: ReactNode }) {
'border-solid border-2 border-primary-accent focus:shadow-none', 'border-solid border-2 border-primary-accent focus:shadow-none',
}, },
}} }}
userProfileMode='navigation' userProfileMode="navigation"
userProfileUrl='/account' userProfileUrl="/account"
signInUrl='/login' signInUrl="/login"
afterSignOutUrl='/' afterSignOutUrl="/"
/> />
); );
} }
+8 -8
View File
@@ -25,14 +25,14 @@ export default function UserPosts({ user }: { user: UserDto }) {
if (pager.isLoading === false && postsWithUser.length === 0) { if (pager.isLoading === false && postsWithUser.length === 0) {
return ( return (
<div className='flex flex-col items-center justify-center border-t border-gray-600 w-full h-full'> <div className="flex flex-col items-center justify-center border-t border-gray-600 w-full h-full">
<div className='flex flex-col items-center gap-2'> <div className="flex flex-col items-center gap-2">
<h2 className='text-xl font-semibold'> <h2 className="text-xl font-semibold">
Hmm...it doesn&apos;t look like you&apos;ve posted anything. Hmm...it doesn&apos;t look like you&apos;ve posted anything.
</h2> </h2>
<Link <Link
href='/posts/add' href="/posts/add"
className='inline-flex max-w-max items-center justify-center px-3 py-1 bg-primary-accent rounded-md text-white' className="inline-flex max-w-max items-center justify-center px-3 py-1 bg-primary-accent rounded-md text-white"
> >
Make a post Make a post
</Link> </Link>
@@ -42,12 +42,12 @@ export default function UserPosts({ user }: { user: UserDto }) {
} }
return ( return (
<div className='flex flex-col items-center w-full'> <div className="flex flex-col items-center w-full">
<div className='flex flex-col items-center w-full'> <div className="flex flex-col items-center w-full">
{postsWithUser.map(post => ( {postsWithUser.map(post => (
<div <div
key={post._id} key={post._id}
className='w-full border border-t-0 border-gray-600 first:border-t last:rounded-b-md p-1 last:mb-4' className="w-full border border-t-0 border-gray-600 last:rounded-b-md p-1 last:mb-4"
> >
<Post post={post} /> <Post post={post} />
</div> </div>
+18 -18
View File
@@ -49,28 +49,28 @@ export default function UserProfile({ user }: { user: UserDto }) {
} }
return ( return (
<div className='w-full'> <div className="w-full">
<div className='shadow-md bg-gradient-to-r from-violet-600 via-violet-600 to-indigo-600 rounded-t-md pt-20 px-5'> <div className="shadow-md bg-gradient-to-r from-violet-600 via-violet-600 to-indigo-600 rounded-t-md pt-20 px-5">
<div className='flex items-center gap-4 -mb-[25px]'> <div className="flex items-center gap-4 -mb-[25px]">
<div className='flex-shrink-0'> <div className="flex-shrink-0">
<Image <Image
alt='user profile image' alt="user profile image"
src={user.clerkImageUrl} src={user.clerkImageUrl}
width={100} width={100}
height={100} height={100}
className='rounded-full object-cover border-4 border-primary-accent' className="rounded-full object-cover border-4 border-primary-accent"
/> />
</div> </div>
<div className='flex flex-col-reverse mb-[25px] flex-1 min-w-0 gap-0.5 md:flex-row md:mb-0 md:gap-2'> <div className="flex flex-col-reverse mb-[25px] flex-1 min-w-0 gap-0.5 md:flex-row md:mb-0 md:gap-2">
<div className='flex flex-col min-w-0 text-white'> <div className="flex flex-col min-w-0 text-white">
<h1 <h1
className='font-bold overflow-hidden text-ellipsis' className="font-bold overflow-hidden text-ellipsis"
title={username} title={username}
> >
{username} {username}
</h1> </h1>
<div className='flex gap-2 items-center text-sm'> <div className="flex gap-2 items-center text-sm">
<AiFillCalendar className='w-4 h-4' /> <AiFillCalendar className="w-4 h-4" />
{`Joined ${monthJoined} ${yearJoined}`} {`Joined ${monthJoined} ${yearJoined}`}
</div> </div>
</div> </div>
@@ -78,7 +78,7 @@ export default function UserProfile({ user }: { user: UserDto }) {
{isLoaded && isSignedIn && currentUser.id !== user.clerkUserId ? ( {isLoaded && isSignedIn && currentUser.id !== user.clerkUserId ? (
<button <button
onClick={handleFollowButtonClick} onClick={handleFollowButtonClick}
type='button' type="button"
className={`py-0.5 px-3 rounded-full text-sm border border-white text-white ${ className={`py-0.5 px-3 rounded-full text-sm border border-white text-white ${
isFollowing ? 'bg-primary-accent' : 'o' isFollowing ? 'bg-primary-accent' : 'o'
}`} }`}
@@ -90,22 +90,22 @@ export default function UserProfile({ user }: { user: UserDto }) {
</div> </div>
</div> </div>
</div> </div>
<div className='flex flex-1 bg-white dark:bg-primary-gray pl-5 border border-gray-600 border-b-0'> <div className="flex flex-1 bg-white dark:bg-primary-gray pl-5 border border-gray-600">
<div className='flex flex-col min-h-[25px] items-start gap-1 py-1 pl-[116px] md:flex-row md:items-center md:gap-4'> <div className="flex flex-col min-h-[25px] items-start gap-1 py-1 pl-[116px] md:flex-row md:items-center md:gap-4">
{countDataLoaded ? ( {countDataLoaded ? (
<> <>
<div> <div>
<span className='font-bold whitespace-nowrap'>{postCount}</span>{' '} <span className="font-bold whitespace-nowrap">{postCount}</span>{' '}
posts posts
</div> </div>
<div className='min-w-0'> <div className="min-w-0">
<span className='font-bold whitespace-nowrap'> <span className="font-bold whitespace-nowrap">
{followerCount} {followerCount}
</span>{' '} </span>{' '}
followers followers
</div> </div>
<div> <div>
<span className='font-bold whitespace-nowrap'> <span className="font-bold whitespace-nowrap">
{followingCount} {followingCount}
</span>{' '} </span>{' '}
following following
+2 -9
View File
@@ -16,10 +16,7 @@ const client = new ConvexReactClient(CONVEX_URL);
export function ConvexProvider({ children }: { children: ReactNode }) { export function ConvexProvider({ children }: { children: ReactNode }) {
return ( return (
<ConvexProviderWithClerk <ConvexProviderWithClerk client={client} useAuth={useAuth}>
client={client}
useAuth={useAuth}
>
{children} {children}
</ConvexProviderWithClerk> </ConvexProviderWithClerk>
); );
@@ -27,11 +24,7 @@ export function ConvexProvider({ children }: { children: ReactNode }) {
export function NextThemesProvider({ children }: { children: ReactNode }) { export function NextThemesProvider({ children }: { children: ReactNode }) {
return ( return (
<ThemeProvider <ThemeProvider attribute="class" defaultTheme="light" enableSystem>
attribute='class'
defaultTheme='light'
enableSystem
>
{children} {children}
</ThemeProvider> </ThemeProvider>
); );