Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
8851da4
feat: Make profile header sticky, standardize dashboard navigation pe…
b-l-i-n-d Dec 24, 2025
b758b4a
feat: Enhance the Button component with an icon-only option.
b-l-i-n-d Dec 24, 2025
671ea8b
feat: Implement dynamic review card rendering with structured data, a…
b-l-i-n-d Dec 24, 2025
d69b3fd
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Dec 26, 2025
22d42b6
fix: Error and clear button not rendering properly
b-l-i-n-d Dec 29, 2025
2d9e39e
feat: new Icons added
b-l-i-n-d Dec 30, 2025
459ec91
feat: Interactive star rating component
b-l-i-n-d Dec 30, 2025
75734cf
feat: allow users to manage their given reviews in the dashboard
b-l-i-n-d Dec 30, 2025
25c62fa
feat: Add frontend dashboard page for managing user reviews with edit…
b-l-i-n-d Dec 30, 2025
9be916f
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Dec 30, 2025
16e5668
feat: Add pagination and empty state to given reviews dashboard and s…
b-l-i-n-d Dec 30, 2025
baa61e4
chore: Update imports
b-l-i-n-d Dec 30, 2025
04f2485
chore: Remove unused codes
b-l-i-n-d Dec 30, 2025
77de185
refactor: Remove unused codes
b-l-i-n-d Dec 30, 2025
d44db6a
refactor: Remove unnecessary exports
b-l-i-n-d Dec 30, 2025
945c6c4
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Jan 1, 2026
4be26a7
refactor: Use tutor-transition mixing
b-l-i-n-d Jan 1, 2026
542256d
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Jan 1, 2026
7857a3d
refactor: reimplement review deletion modal using ConfirmationModal c…
b-l-i-n-d Jan 1, 2026
6876633
feat: Add loading state to delete review button and reload page after…
b-l-i-n-d Jan 1, 2026
c0d18be
feat: swap confirm and cancel button styles and order in Confirmation…
b-l-i-n-d Jan 5, 2026
3aec007
feat: introduce StarRating component and integrate it into the review…
b-l-i-n-d Jan 5, 2026
64aaddc
refactor: rename `showAverage` and `iconSize` methods to `show_averag…
b-l-i-n-d Jan 5, 2026
872369c
chore: remove reviews demo component and its associated template file.
b-l-i-n-d Jan 5, 2026
aac71d7
style: update star rating average and count text styling classes
b-l-i-n-d Jan 5, 2026
78cc51f
refactor: replace `confirm_button` and `cancel_button` methods with `…
b-l-i-n-d Jan 5, 2026
e1a6c44
style: Update confirmation button class from primary to destructive.
b-l-i-n-d Jan 5, 2026
82d07e3
refactor: Align review data structure with WordPress comment fields a…
b-l-i-n-d Jan 5, 2026
095ae21
fix: Use `comment_ID` and `comment_content` for review data instead o…
b-l-i-n-d Jan 5, 2026
5833abf
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Jan 5, 2026
e8211e0
refactor: Refactor star rating components and reviews template
b-l-i-n-d Jan 8, 2026
f4d1dd4
refactor: Store rendered component string in property
b-l-i-n-d Jan 8, 2026
43190b3
feat: Add error message support to InputField component
b-l-i-n-d Jan 8, 2026
3ec9ce8
refactor: Refactor account header to use Button component
b-l-i-n-d Jan 8, 2026
43f7bab
refactor: Refactor dashboard reviews to unify received and given views
b-l-i-n-d Jan 8, 2026
7c6c90e
feat: Add editable state to review card actions and form
b-l-i-n-d Jan 8, 2026
942584d
refactor: Refactor reviews pagination and is_editable logic
b-l-i-n-d Jan 8, 2026
44a296e
feat: Improve initials generation in Avatar component
b-l-i-n-d Jan 8, 2026
e073f6d
feat: Display student info in review cards
b-l-i-n-d Jan 8, 2026
21bdc16
chore: Updated comment for review card
b-l-i-n-d Jan 8, 2026
5a14b1f
refactor: Add review card component and reviews page template
b-l-i-n-d Jan 9, 2026
b2726ac
chore: Add missing newlines at end of template files
b-l-i-n-d Jan 9, 2026
f723969
refactor: Simplify initials processing in Avatar component
b-l-i-n-d Jan 9, 2026
43e69b3
refactor: Replace anchor tags with buttons in profile header
b-l-i-n-d Jan 9, 2026
66e3e4c
chore: Add newline at end of profile header file
b-l-i-n-d Jan 9, 2026
8158355
refactor: Sanitize output and update pagination in reviews
b-l-i-n-d Jan 9, 2026
2fa7cd7
Merge '4.0.0-dev' into 'v4-reviews'
b-l-i-n-d Jan 9, 2026
fe830c4
Merge branch '4.0.0-dev' of https://github.com/themeum/tutor into v4-…
b-l-i-n-d Jan 9, 2026
b88b729
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Jan 13, 2026
52a6b02
chore: add comments explaining phpcs ignore for SVG icon echoes in St…
b-l-i-n-d Jan 16, 2026
82250a1
Merge branch '4.0.0-dev' into v4-reviews
b-l-i-n-d Jan 20, 2026
2419e3f
refactor: Remove PHP_INT_MAX
b-l-i-n-d Jan 20, 2026
b32a0b8
refactor: rename StarRating and StarRatingInput methods to snake_case…
b-l-i-n-d Jan 20, 2026
ae69d94
docs: Add `@since 4.0.0` tags to properties and methods in StarRating…
b-l-i-n-d Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions assets/core/ts/components/star-rating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { __ } from '@wordpress/i18n';

interface StarRatingConfig {
initialRating?: number;
fieldName: string;
}

const starRatingInput = (config: StarRatingConfig) => ({
rating: config.initialRating || 1,
hoverRating: 0,
fieldName: config.fieldName,

get effectiveRating() {
return this.hoverRating > 0 ? this.hoverRating : this.rating;
},

get feedback(): string {
const rating = this.effectiveRating;
if (rating === 0) {
return '';
}

const labels: Record<number, string> = {
1: __('Poor', 'tutor'),
2: __('Fair', 'tutor'),
3: __('Average', 'tutor'),
4: __('Good', 'tutor'),
5: __('Amazing', 'tutor'),
};

if (Number.isInteger(rating)) {
return labels[rating] || '';
}

// Handle fractional ratings (e.g., 4.5 -> "Good / Amazing")
const lower = Math.floor(rating);
const upper = Math.ceil(rating);

const lowerLabel = labels[lower];
const upperLabel = labels[upper];

if (lowerLabel && upperLabel) {
return `${lowerLabel} / ${upperLabel}`;
}

return lowerLabel || upperLabel || '';
},

setRating(val: number, onSet: (rating: number) => void) {
this.rating = val;
onSet(this.rating);
},
});

export const starRatingMeta = {
name: 'starRatingInput',
component: starRatingInput,
};
2 changes: 2 additions & 0 deletions assets/core/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { popoverMeta } from '@Core/ts/components/popover';
import { previewTriggerMeta } from '@Core/ts/components/preview-trigger';
import { selectMeta } from '@Core/ts/components/select';
import { selectDropdownMeta } from '@Core/ts/components/select-dropdown';
import { starRatingMeta } from '@Core/ts/components/star-rating';
import { staticsMeta } from '@Core/ts/components/statics';
import { stepperDropdownMeta } from '@Core/ts/components/stepper-dropdown';
import { tabsMeta } from '@Core/ts/components/tabs';
Expand Down Expand Up @@ -51,6 +52,7 @@ const initializePlugin = () => {
stepperDropdownMeta,
selectMeta,
previewTriggerMeta,
starRatingMeta,
toastMeta,
],
services: [formServiceMeta, modalServiceMeta, queryServiceMeta, toastServiceMeta],
Expand Down
4 changes: 1 addition & 3 deletions assets/icons/star-fill.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 1 addition & 4 deletions assets/icons/star-half.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion assets/src/js/frontend/dashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { initializeHome } from './pages/instructor/home';
import { initializeMyCourses } from './pages/my-courses';
import { initializeOverview } from './pages/overview';
import { initializeQuizAttempts } from './pages/quiz-attempts';
import { initializeReviews } from './pages/reviews';
import { initializeSettings } from './pages/settings';

/**
Expand All @@ -24,7 +25,7 @@ const getCurrentPage = (): string => {
const params = new URLSearchParams(window.location.search);

// Check for subpage parameter - if not 'dashboard', return early
const subpage = params.get('subpage');
const subpage = params.get('page') ? params.get('subpage') : '';
if (subpage && subpage !== 'dashboard') {
return ''; // Not on dashboard, will be handled by early return in initializeDashboard
}
Expand Down Expand Up @@ -63,6 +64,9 @@ const getCurrentPage = (): string => {
if (path.includes('/discussions')) {
return 'discussions';
}
if (path.includes('/reviews')) {
return 'reviews';
}

// Default to home when subpage=dashboard
return 'home';
Expand Down Expand Up @@ -103,6 +107,9 @@ const initializeDashboard = () => {
case 'discussions':
initializeDiscussions();
break;
case 'reviews':
initializeReviews();
break;
default:
// eslint-disable-next-line no-console
console.warn('Unknown dashboard page:', currentPage);
Expand Down
149 changes: 149 additions & 0 deletions assets/src/js/frontend/dashboard/pages/reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { __ } from '@wordpress/i18n';

import { type MutationState } from '@Core/ts/services/Query';

import { wpAjaxInstance } from '@TutorShared/utils/api';
import endpoints from '@TutorShared/utils/endpoints';
import { type TutorMutationResponse } from '@TutorShared/utils/types';
import { convertToErrorMessage } from '@TutorShared/utils/util';

interface ReviewFormProps {
comment_ID?: string;
comment_post_ID: string;
rating: string;
comment_content: string;
}

interface ReviewPayload {
course_id: string;
review_id?: string;
tutor_rating_gen_input: string;
review: string;
}

const reviewDeleteModal = () => {
const query = window.TutorCore.query;

return {
query,
$el: null as HTMLElement | null,
deleteReviewMutation: null as MutationState<unknown> | null,

init() {
if (!this.$el) {
return;
}

this.deleteReviewMutation = this.query.useMutation(this.deleteReview, {
onSuccess: (data: TutorMutationResponse<string>) => {
window.location.reload();
window.TutorCore.toast.success(data?.message ?? __('Review deleted successfully', 'tutor'));
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || __('Failed to delete review', 'tutor'));
},
});
},

async handleDeleteReview(reviewId: string) {
await this.deleteReviewMutation?.mutate(reviewId);
},

async deleteReview(reviewId: string) {
return wpAjaxInstance
.post(endpoints.DELETE_REVIEW, {
review_id: reviewId,
})
.then((res) => res.data);
},
};
};

const reviewCard = (id: string) => {
const query = window.TutorCore.query;

return {
query,
id,
isEditMode: false,
$el: null as HTMLElement | null,
$refs: {} as {
edit: HTMLButtonElement;
delete: HTMLButtonElement;
cancel: HTMLButtonElement;
},
saveRatingMutation: null as MutationState<TutorMutationResponse<string>> | null,

handlers: {} as { [key: string]: EventListener },

init() {
if (!this.$el) {
return;
}

// Bind handlers once to maintain stable references for cleanup
this.handlers.toggleEditMode = () => this.toggleEditMode();

this.$refs.edit?.addEventListener('click', this.handlers.toggleEditMode);
this.$refs.cancel?.addEventListener('click', this.handlers.toggleEditMode);
this.$refs.delete?.addEventListener('click', this.handlers.onDeleteButtonClick);

this.saveRatingMutation = this.query.useMutation(this.saveRating, {
onSuccess: (data: TutorMutationResponse<string>) => {
this.isEditMode = false;
window.TutorCore.toast.success(data.message);
window.location.reload();
},
onError: (error: Error) => {
window.TutorCore.toast.error(convertToErrorMessage(error));
},
});
},

destroy() {
this.$refs.edit?.removeEventListener('click', this.handlers.toggleEditMode);
this.$refs.cancel?.removeEventListener('click', this.handlers.toggleEditMode);
this.$refs.delete?.removeEventListener('click', this.handlers.onDeleteButtonClick);
},

async handleReviewSubmit(data: ReviewFormProps) {
const payload = this.convertFormDataToPayload(data);
await this.saveRatingMutation?.mutate(payload);
},

async saveRating(payload: ReviewPayload) {
return wpAjaxInstance.post(endpoints.PLACE_RATING, payload).then((res) => res.data);
},

convertFormDataToPayload(data: ReviewFormProps): ReviewPayload {
return {
...(data.comment_ID && { comment_id: data.comment_ID }),
course_id: data.comment_post_ID,
tutor_rating_gen_input: data.rating,
review: data.comment_content,
};
},

toggleEditMode() {
this.isEditMode = !this.isEditMode;
},
};
};

const reviewServicesMeta = {
name: 'reviewDeleteModal',
component: reviewDeleteModal,
};

const reviewCardMeta = {
name: 'reviewCard',
component: reviewCard,
};

export const initializeReviews = () => {
window.TutorComponentRegistry.registerAll({
components: [reviewCardMeta, reviewServicesMeta],
});

window.TutorComponentRegistry.initWithAlpine(window.Alpine);
};
4 changes: 4 additions & 0 deletions assets/src/js/v3/shared/utils/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ const endpoints = {
// Announcement
CREATE_ANNOUNCEMENT: 'tutor_announcement_create',
DELETE_ANNOUNCEMENT: 'tutor_announcement_delete',

//Reviews
PLACE_RATING: 'tutor_place_rating',
DELETE_REVIEW: 'delete_tutor_review',
} as const;

export default endpoints;
32 changes: 22 additions & 10 deletions assets/src/scss/frontend/dashboard/_reviews.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
background-color: $tutor-surface-l1;
border: 1px solid $tutor-border-idle;
border-radius: $tutor-radius-lg;
transition-property: background-color, border-color, opacity;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
@include tutor-transition((background-color, border-color, opacity));

&:hover {
background-color: $tutor-surface-l1-hover;
Expand Down Expand Up @@ -59,16 +57,30 @@
&-actions {
@include tutor-flex(row);
gap: $tutor-spacing-4;
@include tutor-transition(opacity);
opacity: 0;

&-button {
@include tutor-button-base;
@include tutor-button-variant(secondary);
@include tutor-button-icon-size(x-small)
}
}

&-text {
@include tutor-typography('p1', 'regular', 'secondary');
}
}

&-form {
@include tutor-flex(column);
background-color: $tutor-surface-l1-hover;
gap: $tutor-spacing-5;
padding: $tutor-spacing-6;

&-fields {
@include tutor-flex(column);
gap: $tutor-spacing-6;
width: 100%;
}
}

&-student {
@include tutor-flex(row, center);
gap: $tutor-spacing-4;
margin-top: $tutor-spacing-4;
}
}
9 changes: 9 additions & 0 deletions assets/src/scss/frontend/dashboard/layout/_profile-pages.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
@use '@Core/scss/mixins' as *;
@use '@Core/scss/tokens' as *;

body:has(#wpadminbar) {
.tutor-profile-header {
top: 32px;
}
}

.tutor-profile-header {
background-color: $tutor-surface-base;
border-bottom: 1px solid $tutor-border-idle;
padding-block: $tutor-spacing-5;
position: sticky;
top: 0;
z-index: $tutor-z-positive;

&-title {
@include tutor-typography(h4, semibold, primary, heading);
Expand Down
2 changes: 1 addition & 1 deletion classes/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function __construct( $register_hooks = true ) {
*
* @since 4.0.0
*/
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), PHP_INT_MAX );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}

/**
Expand Down
Loading
Loading