diff --git a/addon/components/o-s-s/calendar.hbs b/addon/components/o-s-s/calendar.hbs
new file mode 100644
index 000000000..4c57f51e3
--- /dev/null
+++ b/addon/components/o-s-s/calendar.hbs
@@ -0,0 +1,60 @@
+
+
+
+ {{#if (eq this.currentCalendarView "month")}}
+
+
+ {{#each this.weekDays as |day|}}
+
{{day}}
+ {{/each}}
+
+
+
+ {{#each this.calendarDays as |week|}}
+
+ {{#each week as |dayObj|}}
+ {{log dayObj}}
+
+ {{dayObj.day}}
+
+ {{/each}}
+
+ {{/each}}
+
+
+
+ {{else if (eq this.currentCalendarView "months")}}
+
+ {{#each this.monthsOfYear as |month index|}}
+
+ {{month}}
+
+ {{/each}}
+
+ {{/if}}
+
diff --git a/addon/components/o-s-s/calendar.ts b/addon/components/o-s-s/calendar.ts
new file mode 100644
index 000000000..a92154d94
--- /dev/null
+++ b/addon/components/o-s-s/calendar.ts
@@ -0,0 +1,116 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import moment, { type Moment } from 'moment';
+
+enum CalendarViewtype {
+ month = 'month',
+ months = 'months',
+ year = 'year'
+}
+
+export default class CalendarComponent extends Component {
+ @tracked currentDate: Moment = moment();
+ @tracked selectedDay: Moment | null = null;
+ @tracked currentCalendarView: CalendarViewtype = CalendarViewtype.month;
+
+ get yearsRange(): number[] {
+ const currentYear = moment().year();
+ const startYear = currentYear - 20;
+ const endYear = currentYear + 20;
+ return Array.from({ length: endYear - startYear + 1 }, (v, i) => startYear + i);
+ }
+
+ get weekDays(): string[] {
+ return moment.weekdaysShort();
+ }
+
+ get currentMonthName(): string {
+ return this.currentDate.format('MMMM');
+ }
+
+ get currentYear(): number {
+ return this.currentDate.year();
+ }
+
+ get today(): number | null {
+ const today = moment();
+ return today.isSame(this.currentDate, 'month') ? today.date() : null;
+ }
+
+ get monthsOfYear(): string[] {
+ return moment.months();
+ }
+
+ get calendarDays() {
+ const startOfMonth = this.currentDate.clone().startOf('month');
+ const endOfMonth = this.currentDate.clone().endOf('month');
+ const startOfCalendar = startOfMonth.clone().startOf('week');
+ const endOfCalendar = endOfMonth.clone().endOf('week');
+ const today = moment();
+
+ const days = [];
+ let currentDay = startOfCalendar.clone();
+
+ while (currentDay.isBefore(endOfCalendar)) {
+ const week = [];
+ for (let i = 0; i < 7; i++) {
+ week.push({
+ day: currentDay.date(),
+ isCurrentMonth: currentDay.isSame(this.currentDate, 'month'),
+ isToday: currentDay.isSame(today, 'day'),
+ isSelected: this.selectedDay?.isSame(currentDay, 'day')
+ });
+ currentDay.add(1, 'day');
+ }
+ days.push(week);
+ }
+
+ return days;
+ }
+
+ @action
+ selectDate(dayObj: { day: number; isCurrentMonth: boolean; isToday: boolean; isSelected: boolean }) {
+ if (dayObj.isCurrentMonth) {
+ this.selectedDay = this.currentDate.clone().date(dayObj.day);
+ } else if (dayObj.day > 15) {
+ this.previousMonth();
+ this.selectedDay = this.currentDate.clone().endOf('month').date(dayObj.day);
+ } else {
+ this.nextMonth();
+ this.selectedDay = this.currentDate.clone().startOf('month').date(dayObj.day);
+ }
+
+ console.log(this.selectedDay);
+ }
+
+ @action
+ previousMonth() {
+ this.currentDate = this.currentDate.clone().subtract(1, 'month');
+ this.selectedDay = null;
+ }
+
+ @action
+ nextMonth() {
+ this.currentDate = this.currentDate.clone().add(1, 'month');
+ this.selectedDay = null;
+ }
+
+ @action
+ showMonthsView() {
+ this.currentCalendarView = CalendarViewtype.months;
+ }
+
+ @action
+ selectMonth(monthIndex: number) {
+ this.currentDate = this.currentDate.clone().month(monthIndex);
+ this.currentCalendarView = CalendarViewtype.month;
+ }
+
+ @action
+ changeYear(event: Event) {
+ console.log(event);
+ const value = event as unknown as number;
+ this.currentDate = this.currentDate.clone().year(value);
+ }
+}
diff --git a/app/components/o-s-s/calendar.js b/app/components/o-s-s/calendar.js
new file mode 100644
index 000000000..568126b54
--- /dev/null
+++ b/app/components/o-s-s/calendar.js
@@ -0,0 +1 @@
+export { default } from '@upfluence/oss-components/components/o-s-s/calendar';
diff --git a/app/styles/molecules/calendar.less b/app/styles/molecules/calendar.less
new file mode 100644
index 000000000..8b2a2f41b
--- /dev/null
+++ b/app/styles/molecules/calendar.less
@@ -0,0 +1,129 @@
+.oss-calendar {
+ font-family: Arial, sans-serif;
+ max-width: 400px;
+ margin: 20px auto;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ padding: 10px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ .oss-select-container {
+ width: 120px;
+ }
+}
+
+.calendar-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.calendar-header h2 {
+ font-size: 1.5rem;
+ font-weight: bold;
+ margin: 0;
+}
+
+.calendar-table {
+ display: grid;
+ grid-template-rows: auto 1fr;
+}
+
+.calendar-weekdays {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr); /* 7 columns for 7 days */
+ text-align: center;
+ margin-bottom: 5px;
+ font-weight: bold;
+ font-size: 0.9rem;
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #ccc;
+}
+
+.calendar-weekday {
+ padding: 10px 0;
+ border-right: 1px solid #ccc;
+}
+
+.calendar-weekday:last-child {
+ border-right: none;
+}
+
+.calendar-days {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 2px;
+}
+
+.calendar-week {
+ display: contents;
+}
+
+.calendar-day {
+ padding: 10px;
+ text-align: center;
+ cursor: pointer;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: background-color 0.2s, color 0.2s;
+}
+
+.calendar-day:hover {
+ background-color: #f0f0f0;
+}
+
+.calendar-day.selected {
+ background-color: var(--color-primary-500);
+ color: white;
+}
+
+.calendar-day.today {
+ background-color: #ffeeba;
+ font-weight: bold;
+}
+
+.calendar-day.opacified {
+ opacity: 0.4;
+}
+
+.months-picker {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 10px;
+ padding: 10px;
+}
+
+.month-item {
+ padding: 15px;
+ text-align: center;
+ cursor: pointer;
+ background-color: #fff;
+ border: 1px solid var(--color-gray-50);
+ border-radius: 4px;
+ transition: background-color 0.2s;
+}
+
+.month-item:hover {
+ background-color: #f0f0f0;
+}
+
+.calendar-day.highlight {
+ background-color: lighten(@upf-primary-orange, 40%);
+ color: white;
+ font-weight: bold;
+ border: 1px solid lighten(@upf-primary-orange, 40%);
+}
+
+.calendar-day.selected {
+ background-color: var(--color-primary-500);
+ color: white;
+ font-weight: bold;
+ border: 1px solid #007bff;
+}
+
+@media (max-width: 400px) {
+ .calendar-day {
+ padding: 5px;
+ }
+}
diff --git a/app/styles/oss-components.less b/app/styles/oss-components.less
index c5b6356b3..3260ecc34 100644
--- a/app/styles/oss-components.less
+++ b/app/styles/oss-components.less
@@ -50,6 +50,7 @@
@import 'molecules/togglable-section';
@import 'molecules/password-input';
@import 'molecules/avatar-group';
+@import 'molecules/calendar';
@import 'organisms/table';
@import 'organisms/dialog';
@import 'organisms/modal-dialog';
diff --git a/package.json b/package.json
index 686cfb5b4..c208bba5a 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"ember-truth-helpers": "^3.1.1",
"ion-rangeslider": "^2.3.1",
"money-formatter": "^0.1.4",
+ "moment": "^2.29.4",
"resolve": "^1.22.8"
},
"peerDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 13d1ba5f0..c26476591 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -62,6 +62,9 @@ dependencies:
ion-rangeslider:
specifier: ^2.3.1
version: 2.3.1(jquery@3.7.1)
+ moment:
+ specifier: ^2.29.4
+ version: 2.30.1
money-formatter:
specifier: ^0.1.4
version: 0.1.4
@@ -10520,7 +10523,7 @@ packages:
resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==}
engines: {node: '>= 4.0'}
os: [darwin]
- deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2
+ deprecated: Upgrade to fsevents v2 to mitigate potential security issues
requiresBuild: true
dependencies:
bindings: 1.5.0
@@ -13081,6 +13084,10 @@ packages:
resolution: {integrity: sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==}
engines: {node: '>0.9'}
+ /moment@2.30.1:
+ resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+ dev: false
+
/money-formatter@0.1.4:
resolution: {integrity: sha512-0Mw0ztk+nPzyz+m/6lO/ejy2taFpG0OhHyBRRNdpn7EFd0eHHpa053/vNoBPqxVYWd77uJ8BjnVytKMhryMg5g==}
dev: false
diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs
index e60a0032e..0b14d4208 100644
--- a/tests/dummy/app/templates/application.hbs
+++ b/tests/dummy/app/templates/application.hbs
@@ -29,6 +29,7 @@
+
@@ -1110,4 +1111,4 @@
@icon="fa-circle-info"
@skin="error"
/>
-{{/if}}
\ No newline at end of file
+{{/if}}
diff --git a/tests/integration/components/o-s-s/calendar-test.ts b/tests/integration/components/o-s-s/calendar-test.ts
new file mode 100644
index 000000000..0da362559
--- /dev/null
+++ b/tests/integration/components/o-s-s/calendar-test.ts
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | o-s-s/calendar', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function (val) { ... });
+
+ await render(hbs``);
+
+ assert.dom().hasText('');
+
+ // Template block usage:
+ await render(hbs`
+
+ template block text
+
+ `);
+
+ assert.dom().hasText('template block text');
+ });
+});