feat: display user's posts on profile page
This commit is contained in:
+13
-1
@@ -1,5 +1,6 @@
|
|||||||
|
import { paginationOptsValidator } from 'convex/server';
|
||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { mutation } from './_generated/server';
|
import { mutation, query } from './_generated/server';
|
||||||
import { userQuery } from './users';
|
import { userQuery } from './users';
|
||||||
|
|
||||||
export const createPost = mutation({
|
export const createPost = mutation({
|
||||||
@@ -31,3 +32,14 @@ export const createPost = mutation({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getUsersPostById = query({
|
||||||
|
args: { userId: v.id('users'), paginationOpts: paginationOptsValidator },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return await ctx.db
|
||||||
|
.query('posts')
|
||||||
|
.withIndex('by_user_id', q => q.eq('userId', args.userId))
|
||||||
|
.order('desc')
|
||||||
|
.paginate(args.paginationOpts);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
+31
-1
@@ -3,7 +3,37 @@ import { v } from 'convex/values';
|
|||||||
export default defineSchema(
|
export default defineSchema(
|
||||||
{
|
{
|
||||||
users: defineTable({
|
users: defineTable({
|
||||||
clerkUser: v.any(),
|
clerkUser: v.object({
|
||||||
|
id: v.string(),
|
||||||
|
object: v.string(),
|
||||||
|
username: v.union(v.string(), v.null()),
|
||||||
|
first_name: v.string(),
|
||||||
|
last_name: v.string(),
|
||||||
|
gender: v.string(),
|
||||||
|
birthday: v.string(),
|
||||||
|
profile_image_url: v.string(),
|
||||||
|
image_url: v.string(),
|
||||||
|
has_image: v.boolean(),
|
||||||
|
primary_email_address_id: v.boolean(),
|
||||||
|
primary_phone_number_id: v.union(v.string(), v.null()),
|
||||||
|
primary_web3_wallet_id: v.union(v.string(), v.null()),
|
||||||
|
password_enabled: v.boolean(),
|
||||||
|
totp_enabled: v.boolean(),
|
||||||
|
backup_code_enabled: v.boolean(),
|
||||||
|
two_factor_enabled: v.boolean(),
|
||||||
|
banned: v.boolean(),
|
||||||
|
email_addresses: v.any(),
|
||||||
|
phone_numbers: v.any(),
|
||||||
|
web3_wallets: v.any(),
|
||||||
|
external_accounts: v.any(),
|
||||||
|
external_id: v.union(v.string(), v.null()),
|
||||||
|
last_sign_in_at: v.union(v.number(), v.null()),
|
||||||
|
public_metadata: v.any(),
|
||||||
|
private_metadata: v.any(),
|
||||||
|
unsafe_metadata: v.any(),
|
||||||
|
created_at: v.number(),
|
||||||
|
updated_at: v.number(),
|
||||||
|
}),
|
||||||
}).index('by_clerk_id', ['clerkUser.id']),
|
}).index('by_clerk_id', ['clerkUser.id']),
|
||||||
posts: defineTable({
|
posts: defineTable({
|
||||||
parentPostId: v.optional(v.id('posts')),
|
parentPostId: v.optional(v.id('posts')),
|
||||||
|
|||||||
+34
-14
@@ -1,7 +1,37 @@
|
|||||||
import { UserJSON } from '@clerk/nextjs/dist/types/server';
|
|
||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { Doc } from './_generated/dataModel';
|
import {
|
||||||
import { QueryCtx, internalMutation, internalQuery } from './_generated/server';
|
QueryCtx,
|
||||||
|
internalMutation,
|
||||||
|
internalQuery,
|
||||||
|
query,
|
||||||
|
} from './_generated/server';
|
||||||
|
|
||||||
|
export async function userQuery(ctx: QueryCtx, clerkUserId: string) {
|
||||||
|
return await ctx.db
|
||||||
|
.query('users')
|
||||||
|
.withIndex('by_clerk_id', q => q.eq('clerkUser.id', clerkUserId))
|
||||||
|
.unique();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserByClerkId = query({
|
||||||
|
args: { clerkUserId: v.string() },
|
||||||
|
async handler(ctx, args) {
|
||||||
|
return await userQuery(ctx, args.clerkUserId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUserById = query({
|
||||||
|
args: { id: v.id('users') },
|
||||||
|
async handler(ctx, args) {
|
||||||
|
const user = await ctx.db.get(args.id);
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return 'USER_NOT_FOUND';
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const getUser = internalQuery({
|
export const getUser = internalQuery({
|
||||||
args: { subject: v.string() },
|
args: { subject: v.string() },
|
||||||
@@ -10,19 +40,9 @@ export const getUser = internalQuery({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function userQuery(
|
|
||||||
ctx: QueryCtx,
|
|
||||||
clerkUserId: string
|
|
||||||
): Promise<(Omit<Doc<'users'>, 'clerkUser'> & { clerkUser: UserJSON }) | null> {
|
|
||||||
return await ctx.db
|
|
||||||
.query('users')
|
|
||||||
.withIndex('by_clerk_id', q => q.eq('clerkUser.id', clerkUserId))
|
|
||||||
.unique();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateOrCreateUser = internalMutation({
|
export const updateOrCreateUser = internalMutation({
|
||||||
args: { clerkUser: v.any() },
|
args: { clerkUser: v.any() },
|
||||||
async handler(ctx, { clerkUser }: { clerkUser: UserJSON }) {
|
async handler(ctx, { clerkUser }) {
|
||||||
const userRecord = await userQuery(ctx, clerkUser.id);
|
const userRecord = await userQuery(ctx, clerkUser.id);
|
||||||
|
|
||||||
if (userRecord === null) {
|
if (userRecord === null) {
|
||||||
|
|||||||
+13
-3
@@ -1,4 +1,14 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
const { hostname } = require('os');
|
||||||
const nextConfig = {}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
hostname: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
|
|||||||
Generated
+67
-21
@@ -19,6 +19,7 @@
|
|||||||
"@types/node": "20.6.0",
|
"@types/node": "20.6.0",
|
||||||
"@types/react": "18.2.21",
|
"@types/react": "18.2.21",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.7",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"convex": "^1.2.1",
|
"convex": "^1.2.1",
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.7"
|
"encoding": "^0.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@@ -1486,7 +1487,6 @@
|
|||||||
"version": "15.5.7",
|
"version": "15.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.7.tgz",
|
||||||
"integrity": "sha512-bo5fEO5toQeyCp0zVHBeggclqf5SQ/Z5blfFmjwO5dkMVGPgmiwZsJh9nu/Bo5L7IHTuGWrja6LxJVE2uB5ZrQ==",
|
"integrity": "sha512-bo5fEO5toQeyCp0zVHBeggclqf5SQ/Z5blfFmjwO5dkMVGPgmiwZsJh9nu/Bo5L7IHTuGWrja6LxJVE2uB5ZrQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@@ -2272,6 +2272,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/convex/node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
@@ -2450,6 +2469,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/encoding": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
|
"devOptional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.15.0",
|
"version": "5.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
|
||||||
@@ -3529,6 +3557,18 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.2.4",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||||
@@ -5148,25 +5188,6 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
|
||||||
"version": "2.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "4.x || >=6.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch-native": {
|
"node_modules/node-fetch-native": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz",
|
||||||
@@ -6022,6 +6043,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"devOptional": true
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.23.0",
|
"version": "0.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||||
@@ -6350,6 +6377,25 @@
|
|||||||
"whatwg-fetch": "^3.4.1"
|
"whatwg-fetch": "^3.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svix-fetch/node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/swr": {
|
"node_modules/swr": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -20,6 +20,7 @@
|
|||||||
"@types/node": "20.6.0",
|
"@types/node": "20.6.0",
|
||||||
"@types/react": "18.2.21",
|
"@types/react": "18.2.21",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.7",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"convex": "^1.2.1",
|
"convex": "^1.2.1",
|
||||||
@@ -37,6 +38,6 @@
|
|||||||
"svix": "^1.11.0",
|
"svix": "^1.11.0",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"@types/react-syntax-highlighter": "^15.5.7"
|
"encoding": "^0.1.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,29 @@
|
|||||||
|
import UserPosts from '@/components/UserPosts';
|
||||||
|
import UserProfile from '@/components/UserProfile';
|
||||||
|
import { getConvexClient } from '@/lib/convex';
|
||||||
|
import { api } from '../../../../convex/_generated/api';
|
||||||
|
|
||||||
export default async function UserProfilePage({
|
export default async function UserProfilePage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { id: string };
|
params: { id: string };
|
||||||
}) {
|
}) {
|
||||||
// TODO: Profile page. Display profile and user's post
|
const client = await getConvexClient();
|
||||||
return <h1>{params.id}</h1>;
|
const user = await client.query(api.users.getUserByClerkId, {
|
||||||
|
clerkUserId: params.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
// TODO: Show real not found component
|
||||||
|
return <div>Not Found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className='flex flex-col w-full h-full items-center'>
|
||||||
|
<div className='w-full max-w-4xl'>
|
||||||
|
<UserProfile user={user} />
|
||||||
|
<UserPosts user={user} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ import { api } from '../../convex/_generated/api';
|
|||||||
import PostContent from './PostContent';
|
import PostContent from './PostContent';
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const user = useUser();
|
const { user, isSignedIn } = useUser();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const editorTheme = new Compartment();
|
const editorTheme = new Compartment();
|
||||||
const [currentDoc, setCurrentDoc] = useState(['']);
|
const [currentDoc, setCurrentDoc] = useState(['']);
|
||||||
const createPost = useMutation(api.posts.createPost);
|
const createPost = useMutation(api.posts.createPost);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
const doc = [''];
|
const doc = [''];
|
||||||
const extensions = [
|
const extensions = [
|
||||||
@@ -55,13 +56,15 @@ export default function Editor() {
|
|||||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
if (user.isSignedIn !== true) {
|
if (isSignedIn !== true) {
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCreating(true);
|
||||||
|
|
||||||
const result = await createPost({
|
const result = await createPost({
|
||||||
clerkUserId: user.user.id,
|
clerkUserId: user.id,
|
||||||
content: currentDoc,
|
content: currentDoc,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,6 +79,8 @@ export default function Editor() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +126,8 @@ export default function Editor() {
|
|||||||
<div className='flex items-center justify-end p-4 pt-0'>
|
<div className='flex items-center justify-end p-4 pt-0'>
|
||||||
<button
|
<button
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={!currentDoc.join() || !currentDoc.join().trim()}
|
disabled={!currentDoc.join() || !currentDoc.join().trim() || creating}
|
||||||
className='py-1 px-4 bg-primary-accent text-white rounded-md disabled:opacity-50'
|
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>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { SignedIn, SignedOut, UserButton, currentUser } from '@clerk/nextjs';
|
import { SignedIn, SignedOut, currentUser } from '@clerk/nextjs';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { BiSolidMessageSquareAdd } from 'react-icons/bi';
|
import { BiSolidMessageSquareAdd } from 'react-icons/bi';
|
||||||
import ThemeButton from './ThemeButton';
|
import ThemeButton from './ThemeButton';
|
||||||
|
import UserButton from './UserButton';
|
||||||
|
|
||||||
export default async function Navbar() {
|
export default async function Navbar() {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
@@ -50,19 +51,7 @@ export default async function Navbar() {
|
|||||||
</li>
|
</li>
|
||||||
<SignedIn>
|
<SignedIn>
|
||||||
<li>
|
<li>
|
||||||
<UserButton
|
<UserButton />
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
card: 'dark:bg-secondary-gray [&_*]:dark:text-white',
|
|
||||||
userButtonTrigger:
|
|
||||||
'border-solid border-2 border-primary-accent focus:shadow-none',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
userProfileMode='navigation'
|
|
||||||
userProfileUrl='/account'
|
|
||||||
signInUrl='/login'
|
|
||||||
afterSignOutUrl='/'
|
|
||||||
/>
|
|
||||||
</li>
|
</li>
|
||||||
</SignedIn>
|
</SignedIn>
|
||||||
<SignedOut>
|
<SignedOut>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Text } from '@codemirror/state';
|
||||||
|
import { useQuery } from 'convex/react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { api } from '../../convex/_generated/api';
|
||||||
|
import { Doc } from '../../convex/_generated/dataModel';
|
||||||
|
import PostContent from './PostContent';
|
||||||
|
import SpinningLoader from './SpinningLoader';
|
||||||
|
|
||||||
|
export default function Post({ post }: { post: Doc<'posts'> }) {
|
||||||
|
const user = useQuery(api.users.getUserById, { id: post.userId });
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SpinningLoader className='animate-spin w-5 h-5' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user === 'USER_NOT_FOUND') {
|
||||||
|
return <div>User for post not found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = user.clerkUser.username ?? user._id;
|
||||||
|
const createdPostDate = new Date(post._creationTime);
|
||||||
|
const postYear = createdPostDate.getFullYear();
|
||||||
|
const postMonth = createdPostDate.toLocaleString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
const dayOfMonth = createdPostDate.getDate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex w-full h-full gap-4 p-8 bg-white dark:bg-secondary-gray border border-gray-600'>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
alt='user profile image'
|
||||||
|
src={user.clerkUser.image_url}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className='rounded-full object-cover border-4 border-primary-accent'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div>{username}</div>
|
||||||
|
<div className='text-xs'>{`${postMonth} ${dayOfMonth}, ${postYear}`}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<PostContent content={Text.of(post.content).toString()} />
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ImSpinner2 } from 'react-icons/im';
|
||||||
|
|
||||||
|
export default function SpinningLoader({ className }: { className: string }) {
|
||||||
|
return <ImSpinner2 className={className} />;
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMountedEffect } from '@/hooks';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BsMoonStarsFill, BsSunFill } from 'react-icons/bs';
|
import { BsMoonStarsFill, BsSunFill } from 'react-icons/bs';
|
||||||
import { ImSpinner2 } from 'react-icons/im';
|
import { ImSpinner2 } from 'react-icons/im';
|
||||||
|
|
||||||
export default function ThemeButton() {
|
export default function ThemeButton() {
|
||||||
const [mounted, setMounted] = useState(false);
|
const { mounted, Loader } = useMountedEffect();
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => setMounted(true), []);
|
|
||||||
|
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -19,7 +18,7 @@ export default function ThemeButton() {
|
|||||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||||
>
|
>
|
||||||
{mounted === false ? (
|
{mounted === false ? (
|
||||||
<ImSpinner2 className='w-5 h-5 animate-spin' />
|
<Loader className='animate-spin w-5 h-5' />
|
||||||
) : isDark ? (
|
) : isDark ? (
|
||||||
<BsMoonStarsFill className='w-5 h-5' />
|
<BsMoonStarsFill className='w-5 h-5' />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
import { useMountedEffect } from '@/hooks';
|
||||||
|
import { UserButton as ClerkUserButton } from '@clerk/nextjs';
|
||||||
|
|
||||||
|
export default function UserButton() {
|
||||||
|
const { mounted } = useMountedEffect();
|
||||||
|
|
||||||
|
if (mounted === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClerkUserButton
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
card: 'dark:bg-secondary-gray [&_*]:dark:text-white',
|
||||||
|
userButtonTrigger:
|
||||||
|
'border-solid border-2 border-primary-accent focus:shadow-none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
userProfileMode='navigation'
|
||||||
|
userProfileUrl='/account'
|
||||||
|
signInUrl='/login'
|
||||||
|
afterSignOutUrl='/'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePaginatedQuery } from 'convex/react';
|
||||||
|
import { api } from '../../convex/_generated/api';
|
||||||
|
import { Doc } from '../../convex/_generated/dataModel';
|
||||||
|
import Post from './Post';
|
||||||
|
|
||||||
|
export default function UserPosts({ user }: { user: Doc<'users'> }) {
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
const pager = usePaginatedQuery(
|
||||||
|
api.posts.getUsersPostById,
|
||||||
|
{
|
||||||
|
userId: user._id,
|
||||||
|
},
|
||||||
|
{ initialNumItems: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col w-full h-full'>
|
||||||
|
{pager.results.map(post => (
|
||||||
|
<Post
|
||||||
|
key={post._id}
|
||||||
|
post={post}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{pager.isLoading ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => pager.loadMore(10)}
|
||||||
|
disabled={pager.status !== 'CanLoadMore'}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
import { AiFillCalendar } from 'react-icons/ai';
|
||||||
|
import { Doc } from '../../convex/_generated/dataModel';
|
||||||
|
|
||||||
|
export default function UserProfile({ user }: { user: Doc<'users'> }) {
|
||||||
|
const username = user.clerkUser.username ?? user._id;
|
||||||
|
const userCreatedDate = new Date(user._creationTime);
|
||||||
|
const monthJoined = userCreatedDate.toLocaleString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
const yearJoined = userCreatedDate.getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 pl-5 pr-40'>
|
||||||
|
<div className='flex items-center gap-4 -mb-[25px]'>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
alt='user profile image'
|
||||||
|
src={user.clerkUser.image_url}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className='rounded-full object-cover border-4 border-primary-accent'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className='font-bold'>{username}</h1>
|
||||||
|
<div className='flex gap-2 items-center text-sm'>
|
||||||
|
<AiFillCalendar className='w-4 h-4' />
|
||||||
|
{`Joined ${monthJoined} ${yearJoined}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='h-[50px] bg-white dark:bg-primary-gray pl-5 pr-40 border border-gray-600'></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import SpinningLoader from '@/components/SpinningLoader';
|
||||||
import { EditorState, Extension, Text } from '@codemirror/state';
|
import { EditorState, Extension, Text } from '@codemirror/state';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
@@ -30,7 +31,15 @@ export function useCodeMirror({
|
|||||||
setEditorView(view);
|
setEditorView(view);
|
||||||
return () => view.destroy();
|
return () => view.destroy();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [editorRef]);
|
}, [editorRef.current]);
|
||||||
|
|
||||||
return { editorRef, editorView };
|
return { editorRef, editorView };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMountedEffect() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
return { mounted, Loader: SpinningLoader };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { auth } from '@clerk/nextjs';
|
||||||
|
import { ConvexHttpClient } from 'convex/browser';
|
||||||
|
|
||||||
|
export async function getConvexClient() {
|
||||||
|
const { user, getToken } = auth();
|
||||||
|
const token = await getToken({ template: 'convex' });
|
||||||
|
|
||||||
|
if (token === null || user === null) {
|
||||||
|
throw new Error('NOT_AUTHORIZED');
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||||
|
|
||||||
|
if (CONVEX_URL === undefined) {
|
||||||
|
throw new Error('NEXT_PUBLIC_CONVEX_URL is undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ConvexHttpClient(CONVEX_URL);
|
||||||
|
client.setAuth(token);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function getFullMonth() {
|
||||||
|
const monthMap = {};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user