Repository: js-tasks-ru/vue-20250304_shishkovetsroman
Pull request #8: solve meetupView task
Author: login: SwpRoman
Created: 2025-03-15T14:09:26Z | Merged: 2025-03-20T20:19:05Z



Diff:
diff --git a/03-components/10-MeetupView/MeetupView.js b/03-components/10-MeetupView/MeetupView.js
index c2715d1..a74d44b 100644
--- a/03-components/10-MeetupView/MeetupView.js
+++ b/03-components/10-MeetupView/MeetupView.js
@@ -12,31 +12,39 @@ export default defineComponent({
   components: {
     UiAlert,
     UiContainer,
+    MeetupAgenda,
+    MeetupDescription,
+    MeetupCover,
+    MeetupInfo
+  },
+  props: {
+    meetup: {
+      required: true,
+      type: Object
+    }
   },
 
   template: `
     <div>
-
-      <!-- Обложка митапа -->
+      <MeetupCover :title="meetup.title" :image="meetup.image" />
 
       <UiContainer>
         <div class="meetup">
           <div class="meetup__content">
-            <h2>Описание</h2>
 
-            <!-- Описание митапа -->
+            <h2>Описание</h2>
+            <MeetupDescription :description="meetup.description" />
 
             <h2>Программа</h2>
+            <MeetupAgenda v-if="meetup.agenda.length" :agenda="meetup.agenda" />
 
-            <!-- Программа митапа -->
-            <!-- Или при пустой программе - сообщение "Программа пока пуста..." в UiAlert -->
-            <UiAlert></UiAlert>
+            <UiAlert v-else>Программа пока пуста...</UiAlert>
 
           </div>
           <div class="meetup__aside">
 
             <!-- Краткая информация о митапе -->
-
+            <MeetupInfo :date="meetup.date" :place="meetup.place" :organizer="meetup.organizer" />
             <div class="meetup__aside-buttons"></div>
           </div>
         </div>
diff --git a/03-components/20-UiClock/UiClock.js b/03-components/20-UiClock/UiClock.js
index 8215f32..d65c048 100644
--- a/03-components/20-UiClock/UiClock.js
+++ b/03-components/20-UiClock/UiClock.js
@@ -1,9 +1,32 @@
-import { defineComponent } from 'vue'
+import { defineComponent, onUnmounted, onMounted, ref } from 'vue'
+
+const getCurrentTime = () => {
+
+  const timeFormatter = Intl.DateTimeFormat(navigator.language, {
+    timeStyle: "medium",
+  })
+  return timeFormatter.format(Date.now())
+}
 
 export default defineComponent({
   name: 'UiClock',
 
-  setup() {},
+  setup() {
+    let intervalId;
+    const currentTime = ref(getCurrentTime());
+
+    onMounted(() => {
+      intervalId = setInterval(() => currentTime.value = getCurrentTime(), 1000)
+    })
+
+    onUnmounted(() => {
+      clearInterval(intervalId)
+    })
+
+    return {
+      currentTime
+    }
+  },
 
-  template: `<div class="clock">10:12:02</div>`,
+  template: `<div class="clock">{{ currentTime }}</div>`,
 })
diff --git a/03-components/30-removable-emails/EmailList.js b/03-components/30-removable-emails/EmailList.js
index 7c8096e..3d2adfa 100644
--- a/03-components/30-removable-emails/EmailList.js
+++ b/03-components/30-removable-emails/EmailList.js
@@ -3,7 +3,7 @@ import EmailListItem from './EmailListItem.js'
 
 export default defineComponent({
   name: 'EmailList',
-
+  emits: ['delete'],
   components: {
     EmailListItem,
   },
@@ -14,7 +14,7 @@ export default defineComponent({
       required: true,
     },
   },
-
+  
   template: `
     <ul class="emails-list unstyled-list" aria-label="Emails">
       <EmailListItem
@@ -22,6 +22,7 @@ export default defineComponent({
         :key="email"
         :email="email"
         :marked="isMarked"
+        @delete="$emit('delete', index)"
       />
     </ul>
   `,
diff --git a/03-components/30-removable-emails/EmailListItem.js b/03-components/30-removable-emails/EmailListItem.js
index c88ef26..c46a984 100644
--- a/03-components/30-removable-emails/EmailListItem.js
+++ b/03-components/30-removable-emails/EmailListItem.js
@@ -2,23 +2,23 @@ import { defineComponent } from 'vue'
 
 export default defineComponent({
   name: 'EmailListItem',
-
+  emits: ['delete'],
   props: {
     email: {
       type: String,
       required: true,
     },
-
     marked: {
       type: Boolean,
       default: false,
     },
   },
+  
 
   template: `
     <li :class="{ marked }">
       {{ email }}
-      <button type="button" aria-label="Удалить" @click.stop>❌</button>
+      <button type="button" aria-label="Удалить" @click="$emit('delete')">❌</button>
     </li>
   `,
 })
diff --git a/03-components/30-removable-emails/MarkedEmailsApp.js b/03-components/30-removable-emails/MarkedEmailsApp.js
index 5e77151..7ee8165 100644
--- a/03-components/30-removable-emails/MarkedEmailsApp.js
+++ b/03-components/30-removable-emails/MarkedEmailsApp.js
@@ -69,7 +69,7 @@ export default defineComponent({
       <UiFormGroup>
         <UiInput v-model.trim="query" type="search" placeholder="Поиск" aria-label="Поиск" small />
       </UiFormGroup>
-      <EmailList :emails="markedEmails" />
+      <EmailList :emails="markedEmails" @delete="removeEmailByIndex" />
     </div>
   `,
 })
diff --git a/03-components/40-UiCounter/UiCounter.js b/03-components/40-UiCounter/UiCounter.js
index 48ae865..34133b1 100644
--- a/03-components/40-UiCounter/UiCounter.js
+++ b/03-components/40-UiCounter/UiCounter.js
@@ -5,19 +5,46 @@ import './UiCounter.css'
 export default defineComponent({
   name: 'UiCounter',
 
+
   components: {
     UiButton,
   },
+  props: {
+    count: {
+      type: Number,
+      default: 0
+    },
+    min: {
+      type: Number,
+      default: 0,
+    },
+    max: {
+      type: Number,
+      default: Infinity,
+    },
+  },
+
 
-  setup() {
+  setup(props, { emit }) {
     // Рекомендуется для практики реализовать обработку событий внутри setup, а не непосредственно в шаблоне
+    function increment() {
+      emit('update:count', props.count + 1)
+    }
+    
+    function decrement() {
+      emit('update:count', props.count - 1)
+    }
+    return {
+      decrement,
+      increment
+    }
   },
 
   template: `
     <div class="counter">
-      <UiButton aria-label="Decrement" disabled>➖</UiButton>
-      <span class="count" data-testid="count">3</span>
-      <UiButton aria-label="Increment">➕</UiButton>
+      <UiButton aria-label="Decrement" :disabled="count == min" @click="decrement">➖</UiButton>
+      <span class="count" data-testid="count">{{ count }}</span>
+      <UiButton aria-label="Increment" :disabled="count == max" @click="increment">➕</UiButton>
     </div>
   `,
 })
diff --git a/03-components/50-weather-components/UiAlert.js b/03-components/50-weather-components/UiAlert.js
new file mode 100644
index 0000000..9ae079e
--- /dev/null
+++ b/03-components/50-weather-components/UiAlert.js
@@ -0,0 +1,25 @@
+import { defineComponent } from "vue";
+import './WeatherApp.css'
+
+export default defineComponent({
+    name: 'UiAlert',
+    props: {
+        alertSender: {
+            type: String,
+            required: true,
+        },
+        alertDescription: {
+            type: String,
+            required: true,
+        },
+        alertIcon: {
+            type: String,
+            required: true
+        }
+    },
+    template: `
+    <div>
+      <span class="weather-alert__icon">{{ alertIcon }}</span>
+      <span class="weather-alert__description">{{ alertSender }}: {{ alertDescription }}</span>
+    </div>`
+})
\ No newline at end of file
diff --git a/03-components/50-weather-components/UiDetailItem.js b/03-components/50-weather-components/UiDetailItem.js
new file mode 100644
index 0000000..32bdba9
--- /dev/null
+++ b/03-components/50-weather-components/UiDetailItem.js
@@ -0,0 +1,23 @@
+import { defineComponent } from "vue";
+import './WeatherApp.css'
+
+export default defineComponent({
+    name: 'UiDetailItem',
+
+    props: {
+        label: {
+            required: true,
+            type: String,
+        },
+        detailValue: {
+            required: true,
+            type: String
+        }
+    },
+
+    template: `
+            <div>
+              <div class="weather-details__item-label">{{ label }}.</div>
+              <div class="weather-details__item-value">{{ detailValue }}</div>
+            </div>`
+})
\ No newline at end of file
diff --git a/03-components/50-weather-components/UiDetailsList.js b/03-components/50-weather-components/UiDetailsList.js
new file mode 100644
index 0000000..ed34d8d
--- /dev/null
+++ b/03-components/50-weather-components/UiDetailsList.js
@@ -0,0 +1,22 @@
+import { defineComponent } from "vue";
+import './WeatherApp.css'
+import UiDetailItem from "./UiDetailItem";
+
+
+
+export default defineComponent({
+    name: 'UiDetailsList',
+    components: {
+      UiDetailItem
+    },
+    props: {
+        listData: {
+            required: true,
+            type: Array,
+        }
+    },
+
+    template: `<div>
+            <UiDetailItem class="weather-details__item" v-for="item in listData" :label="item.label" :detailValue="String(item.value)" />
+          </div>`
+})
\ No newline at end of file
diff --git a/03-components/50-weather-components/WeatherApp.js b/03-components/50-weather-components/WeatherApp.js
index 5fb34c3..7015701 100644
--- a/03-components/50-weather-components/WeatherApp.js
+++ b/03-components/50-weather-components/WeatherApp.js
@@ -1,52 +1,24 @@
-import { defineComponent } from 'vue'
-import { getWeatherData, WeatherConditionIcons } from './weather.service.ts'
+import { defineComponent, ref } from 'vue'
+import { getWeatherData, } from './weather.service.ts'
 import './WeatherApp.css'
+import WeatherList from './WeatherList.js';
+
 
 export default defineComponent({
   name: 'WeatherApp',
-
+  components: {
+    WeatherList
+  },
+  setup() {
+    const weatherData = ref(getWeatherData())
+    return {
+      weatherData
+    }
+  },
   template: `
     <div>
       <h1 class="title">Погода в Средиземье</h1>
-
-      <ul class="weather-list unstyled-list">
-        <li class="weather-card weather-card--night">
-          <div class="weather-alert">
-            <span class="weather-alert__icon">⚠️</span>
-            <span class="weather-alert__description">Королевская метеослужба короля Арагорна II: Предвещается наступление сильного шторма.</span>
-          </div>
-          <div>
-            <h2 class="weather-card__name">
-              Гондор
-            </h2>
-            <div class="weather-card__time">
-              07:17
-            </div>
-          </div>
-          <div class="weather-conditions">
-            <div class="weather-conditions__icon" title="thunderstorm with heavy rain">⛈️</div>
-            <div class="weather-conditions__temp">15.0 °C</div>
-          </div>
-          <div class="weather-details">
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Давление, мм рт. ст.</div>
-              <div class="weather-details__item-value">754</div>
-            </div>
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Влажность, %</div>
-              <div class="weather-details__item-value">90</div>
-            </div>
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Облачность, %</div>
-              <div class="weather-details__item-value">100</div>
-            </div>
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Ветер, м/с</div>
-              <div class="weather-details__item-value">10.5</div>
-            </div>
-          </div>
-        </li>
-      </ul>
+      <WeatherList :weather-data="weatherData" />
     </div>
   `,
 })
diff --git a/03-components/50-weather-components/WeatherList.js b/03-components/50-weather-components/WeatherList.js
new file mode 100644
index 0000000..a26b6ce
--- /dev/null
+++ b/03-components/50-weather-components/WeatherList.js
@@ -0,0 +1,22 @@
+import { defineComponent } from "vue";
+import './WeatherApp.css'
+import WeatherListItem from "./WeatherListItem.js";
+
+
+
+export default defineComponent({
+  name: 'WeatherList',
+  components: {
+    WeatherListItem
+  },
+  props: {
+    weatherData: {
+      required: true,
+      type: Array,
+    }
+  },
+
+  template: `<ul class="weather-list unstyled-list">
+        <WeatherListItem v-for="weatherItem in weatherData" :key="weatherItem.geographic_name" :weather-item-data="weatherItem" />
+      </ul>`
+})
\ No newline at end of file
diff --git a/03-components/50-weather-components/WeatherListItem.js b/03-components/50-weather-components/WeatherListItem.js
new file mode 100644
index 0000000..3d4b540
--- /dev/null
+++ b/03-components/50-weather-components/WeatherListItem.js
@@ -0,0 +1,82 @@
+import { computed, defineComponent } from "vue";
+import './WeatherApp.css'
+import { WeatherConditionIcons } from './weather.service.ts'
+import UiDetailsList from "./UiDetailsList.js";
+import UiAlert from "./UiAlert.js";
+
+const FREEZING_POINT = 273.15;
+const HPA_VALUE = 0.75;
+
+const isNightNow = (currentTime, sunriseTime, sunsetTime) => {
+  const [currentHours, currentMinutes] = currentTime.split(':').map(Number);
+  const [sunriseHours, sunriseMinutes] = sunriseTime.split(':').map(Number);
+  const [sunsetHours, sunsetMinutes] = sunsetTime.split(':').map(Number);
+
+  const currentDate = new Date();
+  currentDate.setHours(currentHours, currentMinutes, 0, 0);
+
+  const sunriseDate = new Date();
+  sunriseDate.setHours(sunriseHours, sunriseMinutes, 0, 0);
+
+  const sunsetDate = new Date();
+  sunsetDate.setHours(sunsetHours, sunsetMinutes, 0, 0);
+
+  if (currentDate >= sunriseDate && currentDate < sunsetDate) return false
+
+  return true
+}
+
+function getWeatherIcon(id) {
+  return WeatherConditionIcons[id]
+}
+
+const formatKelvinToCelsius = (temperature) => (+temperature - FREEZING_POINT).toFixed(1)
+const formatPressure = (pressure) => (pressure * HPA_VALUE).toFixed(0)
+
+
+
+export default defineComponent({
+  name: 'WeatherListItem',
+  components: {
+    UiDetailsList, UiAlert
+  },
+  props: {
+    weatherItemData: {
+      required: true,
+      type: Object,
+    }
+  },
+  setup(props) {
+    const weatherDetails = computed(() => [
+      { label: 'Давление, мм рт. ст.', value: formatPressure(props.weatherItemData.current.pressure) },
+      { label: 'Влажность, %', value: props.weatherItemData.current.humidity },
+      { label: 'Облачность, %', value: props.weatherItemData.current.clouds },
+      { label: 'Ветер, м/с', value: props.weatherItemData.current.wind_speed },
+    ])
+    return {
+      getWeatherIcon,
+      formatKelvinToCelsius,
+      formatPressure,
+      weatherDetails,
+      isNightNow
+    }
+  },
+  template: `
+        <li class="weather-card" :class="{'weather-card--night': isNightNow(weatherItemData.current.dt, weatherItemData.current.sunrise, weatherItemData.current.sunset) }">
+          <UiAlert v-if="weatherItemData.alert" class="weather-alert" alert-icon="⚠️" :alert-sender="weatherItemData.alert.sender_name" :alert-description="weatherItemData.alert.description" />
+
+          <div>
+            <h2 class="weather-card__name">
+              {{ weatherItemData.geographic_name }}
+            </h2>
+            <div class="weather-card__time">
+              {{ weatherItemData.current.dt }}
+            </div>
+          </div>
+          <div class="weather-conditions">
+            <div class="weather-conditions__icon" :title="weatherItemData.current.weather.description">{{ getWeatherIcon([weatherItemData.current.weather.id]) }} </div>
+            <div class="weather-conditions__temp">{{ formatKelvinToCelsius(weatherItemData.current.temp) }} °C</div>
+          </div>
+          <UiDetailsList class="weather-details" :list-data="weatherDetails" />
+        </li>`
+})
\ No newline at end of file


Reviews:
user: login: ShGKme
body: В `03-components/50-weather-components` требуется немного переработать компоненты, остальные задачи приняты.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:58:31Z
user: login: ShGKme
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T20:17:47Z

Review comments:
diff_hunk: @@ -1,52 +1,24 @@
-import { defineComponent } from 'vue'
-import { getWeatherData, WeatherConditionIcons } from './weather.service.ts'
+import { defineComponent, ref } from 'vue'
+import { getWeatherData, } from './weather.service.ts'
 import './WeatherApp.css'
+import WeatherList from './WeatherList.js';
+
 
 export default defineComponent({
   name: 'WeatherApp',
-
+  components: {
+    WeatherList
+  },
+  setup() {
+    const weatherData = ref(getWeatherData())
+    return {
+      weatherData
+    }
+  },
   template: `
     <div>
       <h1 class="title">Погода в Средиземье</h1>
-
-      <ul class="weather-list unstyled-list">
-        <li class="weather-card weather-card--night">
-          <div class="weather-alert">
-            <span class="weather-alert__icon">⚠️</span>
-            <span class="weather-alert__description">Королевская метеослужба короля Арагорна II: Предвещается наступление сильного шторма.</span>
-          </div>
-          <div>
-            <h2 class="weather-card__name">
-              Гондор
-            </h2>
-            <div class="weather-card__time">
-              07:17
-            </div>
-          </div>
-          <div class="weather-conditions">
-            <div class="weather-conditions__icon" title="thunderstorm with heavy rain">⛈️</div>
-            <div class="weather-conditions__temp">15.0 °C</div>
-          </div>
-          <div class="weather-details">
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Давление, мм рт. ст.</div>
-              <div class="weather-details__item-value">754</div>
-            </div>
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Влажность, %</div>
-              <div class="weather-details__item-value">90</div>
-            </div>
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Облачность, %</div>
-              <div class="weather-details__item-value">100</div>
-            </div>
-            <div class="weather-details__item">
-              <div class="weather-details__item-label">Ветер, м/с</div>
-              <div class="weather-details__item-value">10.5</div>
-            </div>
-          </div>
-        </li>
-      </ul>
+      <WeatherList class="weather-list unstyled-list" :weather-data="weatherData" />
path: 03-components/50-weather-components/WeatherApp.js
user: login: ShGKme
body: Эти классы также должны были быть в `WeatherList`
created_at: 2025-03-19T20:54:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { defineComponent } from "vue";
+import './WeatherApp.css'
+import UiAlert from "./UiAlert.js";
+import WeatherListItem from "./WeatherListItem.js";
+
+
+const isNightNow = (currentTime, sunriseTime, sunsetTime) => {
+  const [currentHours, currentMinutes] = currentTime.split(':').map(Number);
+  const [sunriseHours, sunriseMinutes] = sunriseTime.split(':').map(Number);
+  const [sunsetHours, sunsetMinutes] = sunsetTime.split(':').map(Number);
+
+  const currentDate = new Date();
+  currentDate.setHours(currentHours, currentMinutes, 0, 0);
+
+  const sunriseDate = new Date();
+  sunriseDate.setHours(sunriseHours, sunriseMinutes, 0, 0);
+
+  const sunsetDate = new Date();
+  sunsetDate.setHours(sunsetHours, sunsetMinutes, 0, 0);
+
+  if (currentDate >= sunriseDate && currentDate < sunsetDate) return false
+
+  return true
+}
+
+export default defineComponent({
+  name: 'WeatherList',
+  components: {
+    UiAlert, WeatherListItem
+  },
+  props: {
+    weatherData: {
+      required: true,
+      type: Array,
+    }
+  },
+  setup() {
+    return {
+      isNightNow
+    }
+  },
+  template: `<ul>
+        <WeatherListItem v-for="weatherItem in weatherData" :key="weatherItem.geographic_name" :weather-item-data="weatherItem" class="weather-card" :class="{'weather-card--night': isNightNow(weatherItem.current.dt, weatherItem.current.sunrise, weatherItem.current.sunset) }">
path: 03-components/50-weather-components/WeatherList.js
user: login: ShGKme
body: Аналогично - класс карточки и определение ночной карточки должно быть частью компонента `WeatherListItem`, так как именно этот компонент отвечает за вывод одного элемента.
created_at: 2025-03-19T20:55:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { defineComponent } from "vue";
+import './WeatherApp.css'
+import UiAlert from "./UiAlert.js";
+import WeatherListItem from "./WeatherListItem.js";
+
+
+const isNightNow = (currentTime, sunriseTime, sunsetTime) => {
+  const [currentHours, currentMinutes] = currentTime.split(':').map(Number);
+  const [sunriseHours, sunriseMinutes] = sunriseTime.split(':').map(Number);
+  const [sunsetHours, sunsetMinutes] = sunsetTime.split(':').map(Number);
+
+  const currentDate = new Date();
+  currentDate.setHours(currentHours, currentMinutes, 0, 0);
+
+  const sunriseDate = new Date();
+  sunriseDate.setHours(sunriseHours, sunriseMinutes, 0, 0);
+
+  const sunsetDate = new Date();
+  sunsetDate.setHours(sunsetHours, sunsetMinutes, 0, 0);
+
+  if (currentDate >= sunriseDate && currentDate < sunsetDate) return false
+
+  return true
+}
+
+export default defineComponent({
+  name: 'WeatherList',
+  components: {
+    UiAlert, WeatherListItem
+  },
+  props: {
+    weatherData: {
+      required: true,
+      type: Array,
+    }
+  },
+  setup() {
+    return {
+      isNightNow
+    }
+  },
+  template: `<ul>
+        <WeatherListItem v-for="weatherItem in weatherData" :key="weatherItem.geographic_name" :weather-item-data="weatherItem" class="weather-card" :class="{'weather-card--night': isNightNow(weatherItem.current.dt, weatherItem.current.sunrise, weatherItem.current.sunset) }">
+          <UiAlert v-if="weatherItem.alert" class="weather-alert" alert-icon="⚠️" :alert-sender="weatherItem.alert.sender_name" :alert-description="weatherItem.alert.description" />
+        </WeatherListItem>
path: 03-components/50-weather-components/WeatherList.js
user: login: ShGKme
body: Также вывод `UiAlert` должен быть внутри компонента, а не передаваться слотом.

Слот здесь имеет смысл, если эта карточка может использоваться по-разному, и в разном использовании потребуется передавать разный контент на место алерта. Где-то это будет Alert, а где-то другой контент.

Но здесь карточка выводит полностью информацию о погоде, и предупреждение - её часть.
created_at: 2025-03-19T20:57:53Z
author_association: CONTRIBUTOR

PR comments:
user: login: jsru-1
created_at: 2025-03-15T14:09:51Z
body: Добавляю преподавателя (@ShGKme) для код-ревью.
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-17T10:30:21Z
body: Решение было обновлено, посмотрим что скажет @ShGKme
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-17T10:32:45Z
body: Решение было обновлено, посмотрим что скажет @ShGKme
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-17T10:59:42Z
body: Решение было обновлено, посмотрим что скажет @ShGKme
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-17T11:33:53Z
body: Решение было обновлено, посмотрим что скажет @ShGKme
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-17T13:34:55Z
body: Решение было обновлено, посмотрим что скажет @ShGKme
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-20T08:21:58Z
body: Добавляю преподавателя (@ShGKme) для код-ревью.
author_association: CONTRIBUTOR