WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit e64a3a9

Browse files
committed
Improve table layout, html editors
1 parent 5b8aeaf commit e64a3a9

File tree

9 files changed

+250
-170
lines changed

9 files changed

+250
-170
lines changed

workspaces/resume-generator/app/generate/page.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { GenerateApplicationForm } from '@/components/widgets/generate-application-form/generate-application-form'
22
import { RecentJobApplications } from '@/components/widgets/recent-job-applications/recent-job-applications'
3-
import { Separator } from '@/components/ui/separator'
43
import gqlClient from '@/gql-client'
54

65
const Generate = async () => {
@@ -13,8 +12,6 @@ const Generate = async () => {
1312
resume: data,
1413
}}
1514
/>
16-
<Separator />
17-
<h4>Recent job applications</h4>
1815
<RecentJobApplications className="flex-1" />
1916
</div>
2017
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Editor from 'react-simple-wysiwyg'
2+
3+
interface Props {
4+
onChange?: (content: string) => void
5+
value?: string
6+
}
7+
8+
export const HtmlEditor = ({ onChange, value }: Props) => {
9+
return (
10+
<Editor
11+
className="[&_ul]:list-disc [&_ul]:ml-5 [&_ol]:list-decimal [&_ol]:ml-5"
12+
value={value}
13+
onChange={(e) => onChange?.(e.target.value)}
14+
/>
15+
)
16+
}

workspaces/resume-generator/components/ui/badge.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const badgeVariants = cva(
1515
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
1616
destructive:
1717
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18+
success:
19+
"border-transparent bg-green-700 text-white [a&]:hover:bg-green-700/90",
20+
danger:
21+
"border-transparent bg-yellow-600 text-white [a&]:hover:bg-yellow-600/90",
1822
outline:
1923
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
2024
},

workspaces/resume-generator/components/ui/data-table.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
'use client'
22

33
import {
4-
ColumnDef,
54
Row,
65
Table,
76
flexRender,
87
getCoreRowModel,
98
getSortedRowModel,
109
useReactTable,
10+
TableOptions,
1111
} from '@tanstack/react-table'
1212

1313
import { TableCell, TableHead, TableRow } from '@/components/ui/table'
@@ -46,29 +46,34 @@ const TableRowComponent = <TData,>(rows: Row<TData>[]) =>
4646
{...props}
4747
>
4848
{row.getVisibleCells().map((cell) => (
49-
<TableCell key={cell.id}>
49+
<TableCell
50+
key={cell.id}
51+
style={{
52+
width: cell.column.getSize(),
53+
}}
54+
>
5055
{flexRender(cell.column.columnDef.cell, cell.getContext())}
5156
</TableCell>
5257
))}
5358
</TableRow>
5459
)
5560
}
5661

57-
interface DataTableProps<TData, TValue> {
58-
columns: ColumnDef<TData, TValue>[]
62+
interface DataTableProps<TData> {
63+
columns: TableOptions<TData>['columns']
5964
data: TData[]
6065
height: number
6166
header?:
6267
| React.ReactNode
6368
| (({ table }: { table: Table<TData> }) => React.ReactNode)
6469
}
6570

66-
export function DataTable<TData, TValue>({
71+
export function DataTable<TData>({
6772
columns,
6873
data,
6974
height,
7075
header,
71-
}: DataTableProps<TData, TValue>) {
76+
}: DataTableProps<TData>) {
7277
const table = useReactTable({
7378
data,
7479
columns,
@@ -99,15 +104,15 @@ export function DataTable<TData, TValue>({
99104
>
100105
<div
101106
className="flex items-center"
102-
{...{
103-
style: header.column.getCanSort()
107+
style={
108+
header.column.getCanSort()
104109
? {
105110
cursor: 'pointer',
106111
userSelect: 'none',
107112
}
108-
: {},
109-
onClick: header.column.getToggleSortingHandler(),
110-
}}
113+
: undefined
114+
}
115+
onClick={header.column.getToggleSortingHandler()}
111116
>
112117
{flexRender(
113118
header.column.columnDef.header,

workspaces/resume-generator/components/widgets/adjust-resume-subform/adjust-resume-subform.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ import { Label } from '../../ui/label'
1919
import { Combobox, ComboboxOption } from '../../ui/combobox'
2020
import { z } from 'zod'
2121
import { adjustFormSchema } from '@/schema/adjust-form'
22+
import dynamic from 'next/dynamic'
23+
24+
const HtmlEditor = dynamic(
25+
() => import('../../common/html-editor').then((mod) => mod.HtmlEditor),
26+
{
27+
ssr: false,
28+
},
29+
)
2230

2331
type FormValues = z.infer<typeof adjustFormSchema>
2432

@@ -447,9 +455,12 @@ export const AdjustResumeSubform = ({ form }: Props) => {
447455
name={`resume.experiences.${index}.description.html`}
448456
render={({ field }) => (
449457
<FormItem className="md:col-span-2">
450-
<FormLabel>Description (HTML)</FormLabel>
458+
<FormLabel>Description</FormLabel>
451459
<FormControl>
452-
<Textarea {...field} rows={4} />
460+
<HtmlEditor
461+
value={field.value ?? ''}
462+
onChange={(content) => field.onChange(content)}
463+
/>
453464
</FormControl>
454465
<FormMessage />
455466
</FormItem>

workspaces/resume-generator/components/widgets/job-applications-table/job-applications-table.tsx

Lines changed: 43 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use client'
22
import { JobApplication } from '@/db/types'
33
import { Button } from '../../ui/button'
4-
import { useRouter } from '@bprogress/next/app'
5-
import { ExternalLink, Loader2 } from 'lucide-react'
6-
import { useMemo, useState } from 'react'
4+
import { Loader2 } from 'lucide-react'
5+
import { useState } from 'react'
76
import {
87
AlertDialog,
98
AlertDialogContent,
@@ -19,10 +18,10 @@ import { Input } from '../../ui/input'
1918
import { useSearch } from '@/hooks/use-search'
2019
import { JobApplicationsTableSkeleton } from './job-applications-table-skeleton'
2120
import { DataTable } from '../../ui/data-table'
22-
import { ColumnDef } from '@tanstack/react-table'
23-
import { Badge } from '../../ui/badge'
2421
import { cn } from '@/lib/utils'
25-
import { Checkbox } from '@/components/ui/checkbox'
22+
import { useJobApplicationsTableColumns } from './use-job-applications-table-columns'
23+
import { Table } from '@tanstack/react-table'
24+
2625
interface Props {
2726
jobApplications: JobApplication[]
2827
className?: string
@@ -46,121 +45,51 @@ export const JobApplicationsTable = ({
4645
})
4746
const [containerHeight, setContainerHeight] = useState<number>(600)
4847

49-
const router = useRouter()
5048
const { mutateAsync: removeJobApplications, isPending: isRemoving } =
5149
useRemoveJobApplications()
5250

5351
const [removingIds, setRemovingIds] = useState<string[]>([])
5452

55-
const columns: ColumnDef<JobApplication>[] = useMemo(() => {
56-
const selectColumn: ColumnDef<JobApplication> = {
57-
id: 'select',
58-
header: ({ table }) => (
59-
<Checkbox
60-
checked={
61-
table.getIsAllPageRowsSelected() ||
62-
(table.getIsSomePageRowsSelected() && 'indeterminate')
63-
}
64-
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
65-
/>
66-
),
67-
cell: ({ row }) => (
68-
<Checkbox
69-
checked={row.getIsSelected()}
70-
onCheckedChange={(value) => row.toggleSelected(!!value)}
71-
/>
72-
),
73-
enableSorting: false,
74-
enableHiding: false,
75-
footer: footer
76-
? ({ table }) => (
77-
<div>
78-
Selected {table.getFilteredSelectedRowModel().rows.length} of{' '}
79-
{jobApplications.length} applications
80-
</div>
81-
)
82-
: undefined,
83-
}
84-
85-
const columns_: ColumnDef<JobApplication>[] = [
86-
{
87-
header: 'Company',
88-
cell: ({ row }) => (
89-
<div className="truncate w-40">
90-
{row.original.companyName ?? 'N/A'}
91-
</div>
92-
),
93-
},
94-
{
95-
header: 'Job Title',
96-
cell: ({ row }) => (
97-
<div className="truncate w-60">{row.original.jobTitle ?? 'N/A'}</div>
98-
),
99-
},
100-
{
101-
header: 'Job Description',
102-
cell: ({ row }) => (
103-
<div className="truncate w-[500px]">
104-
{row.original.jobDescription}
105-
</div>
106-
),
107-
id: 'jobDescription',
108-
},
109-
{
110-
accessorFn: (row) =>
111-
new Intl.DateTimeFormat('en-US').format(
112-
new Date(row.createdAt.toMillis()),
113-
),
114-
header: 'Created At',
115-
},
116-
{
117-
cell: ({ row }) => {
118-
const status = (() => {
119-
if (!row.original.status) {
120-
return row.original.resume ? 'Completed' : 'Pending'
121-
}
122-
return (
123-
row.original.status.slice(0, 1).toUpperCase() +
124-
row.original.status.slice(1)
125-
)
126-
})()
127-
return (
128-
<Badge variant={status === 'Completed' ? 'default' : 'secondary'}>
129-
{status}
130-
</Badge>
131-
)
132-
},
133-
header: 'Status',
134-
},
135-
{
136-
id: 'actions',
137-
cell: ({ row }) => {
138-
const jobApplication = row.original
139-
return (
140-
<div className="text-right">
141-
<Button
142-
onClick={(e) => {
143-
e.preventDefault()
144-
e.stopPropagation()
145-
146-
router.push(`/adjust/${jobApplication.id}`)
147-
}}
148-
variant="outline"
149-
>
150-
<ExternalLink />
151-
</Button>
152-
</div>
153-
)
154-
},
155-
},
156-
]
53+
const columns = useJobApplicationsTableColumns({
54+
jobApplications,
55+
footer,
56+
selectable,
57+
})
15758

158-
if (selectable) {
159-
return [selectColumn, ...columns_]
59+
const renderHeader = ({ table }: { table: Table<JobApplication> }) => {
60+
if (!search) {
61+
return null
16062
}
16163

162-
return columns_
163-
}, [footer, jobApplications.length, router, selectable])
64+
const selectedRows = table.getFilteredSelectedRowModel().rows
65+
return (
66+
<div className="flex items-center justify-between gap-2 h-16">
67+
<Input
68+
className="w-96 max-w-full"
69+
value={searchQuery}
70+
onChange={(e) => setSearchQuery(e.target.value)}
71+
placeholder="Search by job title, company name, or description..."
72+
/>
73+
<Button
74+
className={cn(
75+
selectedRows.length === 0 ? 'opacity-0!' : 'opacity-100',
76+
'transition-opacity duration-300',
77+
)}
78+
variant="destructive"
79+
disabled={selectedRows.length === 0}
80+
onClick={() => {
81+
setRemovingIds(
82+
selectedRows
83+
.map((row) => row.original.id)
84+
.filter((id) => id !== undefined),
85+
)
86+
}}
87+
>
88+
Delete
89+
</Button>
90+
</div>
91+
)
92+
}
16493

16594
if (loading) {
16695
return <JobApplicationsTableSkeleton />
@@ -180,36 +109,7 @@ export const JobApplicationsTable = ({
180109
columns={columns}
181110
data={searchResults}
182111
height={containerHeight}
183-
header={({ table }) => {
184-
const selectedRows = table.getFilteredSelectedRowModel().rows
185-
return search ? (
186-
<div className="flex items-center justify-between gap-2 h-16">
187-
<Input
188-
className="w-96 max-w-full"
189-
value={searchQuery}
190-
onChange={(e) => setSearchQuery(e.target.value)}
191-
placeholder="Search by job title, company name, or description..."
192-
/>
193-
<Button
194-
className={cn(
195-
selectedRows.length === 0 ? 'opacity-0!' : 'opacity-100',
196-
'transition-opacity duration-300',
197-
)}
198-
variant="destructive"
199-
disabled={selectedRows.length === 0}
200-
onClick={() => {
201-
setRemovingIds(
202-
selectedRows
203-
.map((row) => row.original.id)
204-
.filter((id) => id !== undefined),
205-
)
206-
}}
207-
>
208-
Delete
209-
</Button>
210-
</div>
211-
) : null
212-
}}
112+
header={renderHeader}
213113
/>
214114

215115
<AlertDialog

0 commit comments

Comments
 (0)