diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..563ecab3
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+node_modules
+playwright-report
+coverage
+dist
+test-results
diff --git a/Dockerfile b/Dockerfile
index f67fa940..d3d61311 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,16 @@
# build stage
FROM node:lts-alpine AS build-stage
+# Set environment variables for non-interactive npm installs
+ENV NPM_CONFIG_LOGLEVEL warn
+ENV CI true
WORKDIR /app
+COPY package.json pnpm-lock.yaml ./
+RUN npm install -g pnpm && pnpm i --frozen-lockfile
COPY . .
-RUN npm install -g pnpm
-RUN pnpm i --frozen-lockfile
RUN pnpm build
# production stage
-FROM nginx:stable-alpine AS production-stage
+FROM nginxinc/nginx-unprivileged:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
diff --git a/src/tools/case-converter/case-converter.vue b/src/tools/case-converter/case-converter.vue
index d3eb5d2e..77757eeb 100644
--- a/src/tools/case-converter/case-converter.vue
+++ b/src/tools/case-converter/case-converter.vue
@@ -73,6 +73,13 @@ const formats = computed(() => [
label: 'Snakecase:',
value: snakeCase(input.value, baseConfig),
},
+ {
+ label: 'Mockingcase:',
+ value: noCase(input.value, baseConfig)
+ .split('')
+ .map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
+ .join(''),
+ },
]);
const inputLabelAlignmentConfig = {
diff --git a/src/tools/color-converter/color-converter.e2e.spec.ts b/src/tools/color-converter/color-converter.e2e.spec.ts
new file mode 100644
index 00000000..6ab91d7a
--- /dev/null
+++ b/src/tools/color-converter/color-converter.e2e.spec.ts
@@ -0,0 +1,23 @@
+import { expect, test } from '@playwright/test';
+
+test.describe('Tool - Color converter', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/color-converter');
+ });
+
+ test('Has title', async ({ page }) => {
+ await expect(page).toHaveTitle('Color converter - IT Tools');
+ });
+
+ test('Color is converted from its name to other formats', async ({ page }) => {
+ await page.getByTestId('input-name').fill('olive');
+
+ expect(await page.getByTestId('input-name').inputValue()).toEqual('olive');
+ expect(await page.getByTestId('input-hex').inputValue()).toEqual('#808000');
+ expect(await page.getByTestId('input-rgb').inputValue()).toEqual('rgb(128, 128, 0)');
+ expect(await page.getByTestId('input-hsl').inputValue()).toEqual('hsl(60, 100%, 25%)');
+ expect(await page.getByTestId('input-hwb').inputValue()).toEqual('hwb(60 0% 50%)');
+ expect(await page.getByTestId('input-cmyk').inputValue()).toEqual('device-cmyk(0% 0% 100% 50%)');
+ expect(await page.getByTestId('input-lch').inputValue()).toEqual('lch(52.15% 56.81 99.57)');
+ });
+});
diff --git a/src/tools/color-converter/color-converter.models.test.ts b/src/tools/color-converter/color-converter.models.test.ts
new file mode 100644
index 00000000..4261fed1
--- /dev/null
+++ b/src/tools/color-converter/color-converter.models.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest';
+import { removeAlphaChannelWhenOpaque } from './color-converter.models';
+
+describe('color-converter models', () => {
+ describe('removeAlphaChannelWhenOpaque', () => {
+ it('remove alpha channel of an hex color when it is opaque (alpha = 1)', () => {
+ expect(removeAlphaChannelWhenOpaque('#000000ff')).toBe('#000000');
+ expect(removeAlphaChannelWhenOpaque('#ffffffFF')).toBe('#ffffff');
+ expect(removeAlphaChannelWhenOpaque('#000000FE')).toBe('#000000FE');
+ expect(removeAlphaChannelWhenOpaque('#00000000')).toBe('#00000000');
+ });
+ });
+});
diff --git a/src/tools/color-converter/color-converter.models.ts b/src/tools/color-converter/color-converter.models.ts
new file mode 100644
index 00000000..030fd074
--- /dev/null
+++ b/src/tools/color-converter/color-converter.models.ts
@@ -0,0 +1,52 @@
+import { type Colord, colord } from 'colord';
+import { withDefaultOnError } from '@/utils/defaults';
+import { useValidation } from '@/composable/validation';
+
+export { removeAlphaChannelWhenOpaque, buildColorFormat };
+
+function removeAlphaChannelWhenOpaque(hexColor: string) {
+ return hexColor.replace(/^(#(?:[0-9a-f]{3}){1,2})ff$/i, '$1');
+}
+
+function buildColorFormat({
+ label,
+ parse = value => colord(value),
+ format,
+ placeholder,
+ invalidMessage = `Invalid ${label.toLowerCase()} format.`,
+ type = 'text',
+}: {
+ label: string
+ parse?: (value: string) => Colord
+ format: (value: Colord) => string
+ placeholder?: string
+ invalidMessage?: string
+ type?: 'text' | 'color-picker'
+}) {
+ const value = ref('');
+
+ return {
+ type,
+ label,
+ parse: (v: string) => withDefaultOnError(() => parse(v), undefined),
+ format,
+ placeholder,
+ value,
+ validation: useValidation({
+ source: value,
+ rules: [
+ {
+ message: invalidMessage,
+ validator: v => withDefaultOnError(() => {
+ if (v === '') {
+ return true;
+ }
+
+ return parse(v).isValid();
+ }, false),
+ },
+ ],
+ }),
+
+ };
+}
diff --git a/src/tools/color-converter/color-converter.vue b/src/tools/color-converter/color-converter.vue
index 0b9909fa..a7d6139f 100644
--- a/src/tools/color-converter/color-converter.vue
+++ b/src/tools/color-converter/color-converter.vue
@@ -1,87 +1,103 @@
-
-
+
+ updateColorValue(parse(v), key)"
+ />
+
+
onInputUpdated(v, 'hex')"
+ @update:value="(v:string) => updateColorValue(parse(v), key)"
/>
-
- onInputUpdated(v, 'name')" />
-
-
- onInputUpdated(v, 'hex')" />
-
-
- onInputUpdated(v, 'rgb')" />
-
-
- onInputUpdated(v, 'hsl')" />
-
-
- onInputUpdated(v, 'hwb')" />
-
-
- onInputUpdated(v, 'lch')" />
-
-
- onInputUpdated(v, 'cmyk')" />
-
-
+
diff --git a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts
index 34ee7495..249dd754 100644
--- a/src/tools/date-time-converter/date-time-converter.e2e.spec.ts
+++ b/src/tools/date-time-converter/date-time-converter.e2e.spec.ts
@@ -29,5 +29,6 @@ test.describe('Date time converter - json to yaml', () => {
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
+ expect((await page.getByTestId('Excel date/time').inputValue()).trim()).toEqual('45028.88222222222');
});
});
diff --git a/src/tools/date-time-converter/date-time-converter.models.test.ts b/src/tools/date-time-converter/date-time-converter.models.test.ts
index 502cdc67..c2c7bee9 100644
--- a/src/tools/date-time-converter/date-time-converter.models.test.ts
+++ b/src/tools/date-time-converter/date-time-converter.models.test.ts
@@ -1,5 +1,8 @@
import { describe, expect, test } from 'vitest';
import {
+ dateToExcelFormat,
+ excelFormatToDate,
+ isExcelFormat,
isISO8601DateTimeString,
isISO9075DateString,
isMongoObjectId,
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
expect(isMongoObjectId('')).toBe(false);
});
});
+
+ describe('isExcelFormat', () => {
+ test('an Excel format string is a floating number that can be negative', () => {
+ expect(isExcelFormat('0')).toBe(true);
+ expect(isExcelFormat('1')).toBe(true);
+ expect(isExcelFormat('1.1')).toBe(true);
+ expect(isExcelFormat('-1.1')).toBe(true);
+ expect(isExcelFormat('-1')).toBe(true);
+
+ expect(isExcelFormat('')).toBe(false);
+ expect(isExcelFormat('foo')).toBe(false);
+ expect(isExcelFormat('1.1.1')).toBe(false);
+ });
+ });
+
+ describe('dateToExcelFormat', () => {
+ test('a date in Excel format is the number of days since 01/01/1900', () => {
+ expect(dateToExcelFormat(new Date('2016-05-20T00:00:00.000Z'))).toBe('42510');
+ expect(dateToExcelFormat(new Date('2016-05-20T12:00:00.000Z'))).toBe('42510.5');
+ expect(dateToExcelFormat(new Date('2023-10-31T09:26:06.421Z'))).toBe('45230.39312987268');
+ expect(dateToExcelFormat(new Date('1970-01-01T00:00:00.000Z'))).toBe('25569');
+ expect(dateToExcelFormat(new Date('1800-01-01T00:00:00.000Z'))).toBe('-36522');
+ });
+ });
+
+ describe('excelFormatToDate', () => {
+ test('a date in Excel format is the number of days since 01/01/1900', () => {
+ expect(excelFormatToDate('0')).toEqual(new Date('1899-12-30T00:00:00.000Z'));
+ expect(excelFormatToDate('1')).toEqual(new Date('1899-12-31T00:00:00.000Z'));
+ expect(excelFormatToDate('2')).toEqual(new Date('1900-01-01T00:00:00.000Z'));
+ expect(excelFormatToDate('4242.4242')).toEqual(new Date('1911-08-12T10:10:50.880Z'));
+ expect(excelFormatToDate('42738.22626859954')).toEqual(new Date('2017-01-03T05:25:49.607Z'));
+ expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
+ });
+ });
});
diff --git a/src/tools/date-time-converter/date-time-converter.models.ts b/src/tools/date-time-converter/date-time-converter.models.ts
index 173b8a87..f5eedbfa 100644
--- a/src/tools/date-time-converter/date-time-converter.models.ts
+++ b/src/tools/date-time-converter/date-time-converter.models.ts
@@ -9,6 +9,9 @@ export {
isTimestamp,
isUTCDateString,
isMongoObjectId,
+ dateToExcelFormat,
+ excelFormatToDate,
+ isExcelFormat,
};
const ISO8601_REGEX
@@ -21,6 +24,8 @@ const RFC3339_REGEX
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
+const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
+
function createRegexMatcher(regex: RegExp) {
return (date?: string) => !_.isNil(date) && regex.test(date);
}
@@ -33,6 +38,8 @@ const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
+const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
+
function isUTCDateString(date?: string) {
if (_.isNil(date)) {
return false;
@@ -45,3 +52,11 @@ function isUTCDateString(date?: string) {
return false;
}
}
+
+function dateToExcelFormat(date: Date) {
+ return String(((date.getTime()) / (1000 * 60 * 60 * 24)) + 25569);
+}
+
+function excelFormatToDate(excelFormat: string | number) {
+ return new Date((Number(excelFormat) - 25569) * 86400 * 1000);
+}
diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue
index 241c9cf6..5636ed46 100644
--- a/src/tools/date-time-converter/date-time-converter.vue
+++ b/src/tools/date-time-converter/date-time-converter.vue
@@ -14,6 +14,9 @@ import {
} from 'date-fns';
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
import {
+ dateToExcelFormat,
+ excelFormatToDate,
+ isExcelFormat,
isISO8601DateTimeString,
isISO9075DateString,
isMongoObjectId,
@@ -85,6 +88,12 @@ const formats: DateFormat[] = [
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
formatMatcher: date => isMongoObjectId(date),
},
+ {
+ name: 'Excel date/time',
+ fromDate: date => dateToExcelFormat(date),
+ toDate: excelFormatToDate,
+ formatMatcher: isExcelFormat,
+ },
];
const formatIndex = ref(6);