diff --git a/mission/chapter03/mission_ch03/.gitignore b/mission/chapter03/mission_ch03/.gitignore index a547bf3..3b0b403 100644 --- a/mission/chapter03/mission_ch03/.gitignore +++ b/mission/chapter03/mission_ch03/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/eslint.config.js b/mission/chapter03/mission_ch03/eslint.config.js index 91b20da..7825412 100644 --- a/mission/chapter03/mission_ch03/eslint.config.js +++ b/mission/chapter03/mission_ch03/eslint.config.js @@ -34,6 +34,7 @@ export default [ { allowConstantExport: true }, ], 'react/prop-types': 'off', + 'no-unused-vars': ['warn', { 'varsIgnorePattern': 'React' }], }, }, ] diff --git a/mission/chapter03/mission_ch03/package-lock.json b/mission/chapter03/mission_ch03/package-lock.json index 427ddbb..26b7577 100644 --- a/mission/chapter03/mission_ch03/package-lock.json +++ b/mission/chapter03/mission_ch03/package-lock.json @@ -12,6 +12,8 @@ "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", + "react-router-dom": "^6.27.0", "styled-components": "^6.1.13" }, "devDependencies": { @@ -625,6 +627,15 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", @@ -3366,12 +3377,53 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", diff --git a/mission/chapter03/mission_ch03/package.json b/mission/chapter03/mission_ch03/package.json index 23a07c3..2225a6c 100644 --- a/mission/chapter03/mission_ch03/package.json +++ b/mission/chapter03/mission_ch03/package.json @@ -14,6 +14,8 @@ "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", + "react-router-dom": "^6.27.0", "styled-components": "^6.1.13" }, "devDependencies": { diff --git a/mission/chapter03/mission_ch03/src/App.jsx b/mission/chapter03/mission_ch03/src/App.jsx index 5ceb9d5..57fbe76 100644 --- a/mission/chapter03/mission_ch03/src/App.jsx +++ b/mission/chapter03/mission_ch03/src/App.jsx @@ -1,23 +1,50 @@ -//import React from 'react'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import React from 'react'; +import {createBrowserRouter, RouterProvider} from "react-router-dom"; import RootLayout from "./layout/root-layout"; +import HomePage from "./pages/home"; import Login from "./pages/login"; import Signup from "./pages/signup"; -import HomePage from "./pages/home"; -import Movies from "./pages/movies"; +import Movies from "./pages/movie"; +import MovieDetail from "./components/MovieDetail"; +import Search from './pages/search'; + +const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { + index: true, + element: + }, + { + path: 'login', + element: + }, + { + path: 'signup', + element: + }, + { + path: 'movies', + element: + }, + { + path: 'movies/:movieId', + element: + }, + { + path: 'search', + element: + } + ] + }, + +]) function App() { return ( - - - }> - } /> - } /> - } /> - } /> - - - + ); } diff --git a/mission/chapter03/mission_ch03/src/assets/sanrio1.png b/mission/chapter03/mission_ch03/src/assets/sanrio1.png new file mode 100644 index 0000000..39ce477 Binary files /dev/null and b/mission/chapter03/mission_ch03/src/assets/sanrio1.png differ diff --git a/mission/chapter03/mission_ch03/src/assets/sanrio2.png b/mission/chapter03/mission_ch03/src/assets/sanrio2.png new file mode 100644 index 0000000..8300f90 Binary files /dev/null and b/mission/chapter03/mission_ch03/src/assets/sanrio2.png differ diff --git a/mission/chapter03/mission_ch03/src/assets/sanrio3.png b/mission/chapter03/mission_ch03/src/assets/sanrio3.png new file mode 100644 index 0000000..4001e97 Binary files /dev/null and b/mission/chapter03/mission_ch03/src/assets/sanrio3.png differ diff --git a/mission/chapter03/mission_ch03/src/assets/sanrio4.png b/mission/chapter03/mission_ch03/src/assets/sanrio4.png new file mode 100644 index 0000000..2a9f1cc Binary files /dev/null and b/mission/chapter03/mission_ch03/src/assets/sanrio4.png differ diff --git a/mission/chapter03/mission_ch03/src/components/CategoryList.jsx b/mission/chapter03/mission_ch03/src/components/CategoryList.jsx new file mode 100644 index 0000000..104c80d --- /dev/null +++ b/mission/chapter03/mission_ch03/src/components/CategoryList.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { MoviesGrid, MovieCard, MovieImage, MovieLabel } from "../styled/Movie.styled"; + +const CategoryList = ({ categories }) => { + return ( + + {categories.map((category) => ( + + + + {category.title} + + + ))} + + ); +}; + +export default CategoryList; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/components/MovieDetail.jsx b/mission/chapter03/mission_ch03/src/components/MovieDetail.jsx new file mode 100644 index 0000000..58edd70 --- /dev/null +++ b/mission/chapter03/mission_ch03/src/components/MovieDetail.jsx @@ -0,0 +1,29 @@ +// MovieDetail.js +import React from "react"; +import { useParams } from "react-router-dom"; +import useCustomFetch from "../hooks/useCustomFetch"; +import { MovieDetailContainer, MovieTitle, MovieOverview, MoviePoster } from "../styled/MovieDetail.styled"; + +const MovieDetail = () => { + const { movieId } = useParams(); // URL에서 movieId를 가져옴 + const apiKey = import.meta.env.VITE_TMDB_API_KEY; + const { data: movies, isLoading, isError } = useCustomFetch(`https://api.themoviedb.org/3/movie/${movieId}?api_key=${apiKey}&language=ko-KR`); + if (isLoading) return
Loading...
; + if (isError) return
Error loading movie details.
; + + // movie가 undefined가 아닌 경우에만 렌더링 + return ( + + {movies.results?.map((movie) => ( +
+ + {movie.title} + {movie.overview} +
+ ))} +
+ + ); +}; + +export default MovieDetail; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/components/MovieItem.jsx b/mission/chapter03/mission_ch03/src/components/MovieItem.jsx deleted file mode 100644 index bc1236f..0000000 --- a/mission/chapter03/mission_ch03/src/components/MovieItem.jsx +++ /dev/null @@ -1,57 +0,0 @@ -//import React from 'react'; -import styled from 'styled-components'; - -const MovieItemContainer = styled.div` - border-style: none; - padding: 4px; - margin: 4px; - width: 8.5%; - display: inline-block; - vertical-align: top; - text-align: center; -`; - -const ImgContainer = styled.div` - position: relative; - width: 100%; - height: auto; -`; - -const MovieImage = styled.img` - border-radius: 5px; - width: 100%; - height: auto; - display: block; -`; - -const Overlay = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; - transition: opacity 0.15s ease; - background-color: rgba(0, 0, 0, 0.5); - border-radius: 5px; - - ${ImgContainer}:hover & { /* &는 현재 컴포넌트(Overlay) */ - opacity: 1; - } -`; - -function MovieItem({ movie }) { - return ( - - - - - - - ); -} - -export default MovieItem; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/components/MovieList.jsx b/mission/chapter03/mission_ch03/src/components/MovieList.jsx index 3c4377d..411aeaf 100644 --- a/mission/chapter03/mission_ch03/src/components/MovieList.jsx +++ b/mission/chapter03/mission_ch03/src/components/MovieList.jsx @@ -1,15 +1,27 @@ -//import React from 'react'; -import MovieItem from './MovieItem'; -import { MovieListContainer } from './MovieList.styled'; +import React from "react"; +import { Link } from "react-router-dom"; +import { MoviesGrid, MovieCard, MovieImage, MovieTitle } from "../styled/Movie.styled"; -function MovieList({ movies }) { +const MovieList = ({ movies }) => { return ( - + {movies.map((movie) => ( - + + + + {movie.title} + + ))} - + ); -} +}; export default MovieList; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/components/custom-button.jsx b/mission/chapter03/mission_ch03/src/components/custom-button.jsx deleted file mode 100644 index b04b5d5..0000000 --- a/mission/chapter03/mission_ch03/src/components/custom-button.jsx +++ /dev/null @@ -1,9 +0,0 @@ -const CustomButton = () => { - return ( - - ); -}; - -export default CustomButton; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/components/navbar.jsx b/mission/chapter03/mission_ch03/src/components/navbar.jsx index 0ccbdc6..c048e91 100644 --- a/mission/chapter03/mission_ch03/src/components/navbar.jsx +++ b/mission/chapter03/mission_ch03/src/components/navbar.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { NavbarContainer, Logo, NavLinks, NavButton } from "./Navbar.styled"; +import { NavbarContainer, Logo, NavLinks, NavButton } from "../styled/Navbar.styled"; const Navbar = () => { return ( diff --git a/mission/chapter03/mission_ch03/src/components/sidebar.jsx b/mission/chapter03/mission_ch03/src/components/sidebar.jsx index eb539c5..38696a7 100644 --- a/mission/chapter03/mission_ch03/src/components/sidebar.jsx +++ b/mission/chapter03/mission_ch03/src/components/sidebar.jsx @@ -1,11 +1,19 @@ import React from "react"; -import { SidebarContainer, SidebarLink } from "./Sidebar.styled"; +import { SidebarContainer, SidebarLink } from "../styled/Sidebar.styled"; +import { IoSearch } from "react-icons/io5"; +import { PiFilmSlateFill } from "react-icons/pi"; const Sidebar = () => { return ( - 찾기 - 영화 + + {/* 찾기 아이콘 */} + 찾기 + + + {/* 영화 아이콘 */} + 영화 + ); }; diff --git a/mission/chapter03/mission_ch03/src/hooks/useCustomFetch.js b/mission/chapter03/mission_ch03/src/hooks/useCustomFetch.js new file mode 100644 index 0000000..340ef14 --- /dev/null +++ b/mission/chapter03/mission_ch03/src/hooks/useCustomFetch.js @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; + +const useCustomFetch = (url) =>{ + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + useEffect(()=>{ + const fetchData = async()=>{ + setIsLoading(true); + try{ + const response = await axios.get(url); + setData(response.data); + } + catch (error){ + setIsError(error); + } + finally{ + setIsLoading(false); + } + } + fetchData(); + },[url]); + + return {data,isLoading,isError}; +} + +export default useCustomFetch; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/index.css b/mission/chapter03/mission_ch03/src/index.css index e69de29..2deddde 100644 --- a/mission/chapter03/mission_ch03/src/index.css +++ b/mission/chapter03/mission_ch03/src/index.css @@ -0,0 +1,3 @@ +body{ + margin : 0; +} \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/layout/root-layout.jsx b/mission/chapter03/mission_ch03/src/layout/root-layout.jsx index 1b2143f..1036144 100644 --- a/mission/chapter03/mission_ch03/src/layout/root-layout.jsx +++ b/mission/chapter03/mission_ch03/src/layout/root-layout.jsx @@ -1,6 +1,10 @@ +// root-layout : 모든 페이지에 공통적으로 보여지는 요소 +// (헤더, 푸터, 네비게이션 바, 사이드바 등)를 정의 +import React from "react"; +import Navbar from "../components/Navbar.jsx"; +import Sidebar from "../components/Sidebar.jsx"; +import {LayoutContainer, MainContent} from "../styled/RootLayout.styled"; import {Outlet} from "react-router-dom"; -import Navbar from "../components/navbar.jsx"; -import Sidebar from "../components/sidebar.jsx"; // : 부모 컴포넌트 내에서 자식 컴포넌트가 어디에 렌더링될지 지정 // 여기서는 부모 컴포넌트가 RootLayout(path: '/') @@ -9,8 +13,12 @@ const RootLayout = () => { return ( <> - - + + + + + + ); }; diff --git a/mission/chapter03/mission_ch03/src/pages/home.jsx b/mission/chapter03/mission_ch03/src/pages/home.jsx index e69de29..99671ca 100644 --- a/mission/chapter03/mission_ch03/src/pages/home.jsx +++ b/mission/chapter03/mission_ch03/src/pages/home.jsx @@ -0,0 +1,35 @@ +// 첫 화면 +import React, { useEffect, useState } from "react"; +import axios from "axios"; + +const Home = () => { + const [movies, setMovies] = useState([]); + + useEffect(() => { // 컴포넌트가 처음 렌더링될 때 인기 영화 데이터 요청청 + axios + .get(`https://api.themoviedb.org/3/movie/popular`, { + params: { + api_key: import.meta.env.REACT_APP_TMDB_API_KEY, + language: "ko-KR", + page: 1, + }, + }) + .then((response) => setMovies(response.data.results)); + // 응답 결과를 movies 상태에 저장 + }, []); + + return ( +
+ {movies.map((movie) => ( // 각 영화 컴포넌트 +
+ {movie.title} +
+ ))} +
+ ); +}; + +export default Home; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/pages/movie.jsx b/mission/chapter03/mission_ch03/src/pages/movie.jsx index 7b1d2de..a2a9554 100644 --- a/mission/chapter03/mission_ch03/src/pages/movie.jsx +++ b/mission/chapter03/mission_ch03/src/pages/movie.jsx @@ -1,27 +1,64 @@ -//import React from "react"; -import {MoviesContainer, CategoryTitle, MoviesGrid, MovieCard, MovieImage, MovieLabel, MovieTitle, MovieCategory} from "../styled/movie.styled"; +import React, { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import { MoviesContainer, CategoryTitle } from "../styled/Movie.styled"; +import CategoryList from "../components/CategoryList"; +import MovieList from "../components/MovieList"; + +import image1 from '../assets/sanrio1.png'; +import image2 from '../assets/sanrio2.png'; +import image3 from '../assets/sanrio3.png'; +import image4 from '../assets/sanrio4.png'; const Movies = () => { - const movieList = [ - { id: 1, title: "현재 상영중인", imgSrc: "/path/to/image1.jpg" }, - { id: 2, title: "인기있는", imgSrc: "/path/to/image2.jpg" }, - { id: 3, title: "높은 평가를 받은", imgSrc: "/path/to/image3.jpg" }, - { id: 4, title: "개봉 예정중인", imgSrc: "/path/to/image4.jpg" }, + const { category } = useParams(); // URL에서 category 파라미터를 가져옴 + const [movies, setMovies] = useState([]); // 영화 데이터 상태 + const apiKey = import.meta.env.VITE_TMDB_API_KEY; // API 키를 환경 변수에서 가져옴 + const navigate = useNavigate(); + + // 카테고리 목록 + const categories = [ + { id: 1, title: "현재 상영중인", imgSrc: image1, endpoint: "now_playing" }, + { id: 2, title: "인기있는", imgSrc: image2, endpoint: "popular" }, + { id: 3, title: "높은 평가를 받은", imgSrc: image3, endpoint: "top_rated" }, + { id: 4, title: "개봉 예정중인", imgSrc: image4, endpoint: "upcoming" }, ]; + // 영화 데이터를 가져오는 함수 + const fetchMovies = (categoryEndpoint) => { + axios + .get(`https://api.themoviedb.org/3/movie/${categoryEndpoint}`, { + params: { + api_key: apiKey, + language: "ko-KR", + page: 1, + }, + }) + .then((response) => setMovies(response.data.results)) + .catch((error) => console.error("영화 데이터를 가져오는 중 오류 발생:", error)); + }; + + // 카테고리 클릭 시 URL 변경 및 영화 데이터 로드 + const handleCategorySelect = (categoryEndpoint) => { + navigate(`/movies/${categoryEndpoint}`); + }; + + // URL의 category가 변경될 때마다 해당 카테고리의 영화를 불러옴 + useEffect(() => { + if (category) { + fetchMovies(category); + } + }, [category]); + return ( 카테고리 - - {movieList.map((movie) => ( - - - {movie.title} - {movie.title} - {movie.category} - - ))} - + + {/* 카테고리 목록 컴포넌트 */} + + + {/* 선택한 카테고리의 영화 목록 */} + ); }; diff --git a/mission/chapter03/mission_ch03/src/styled/MovieDetail.styled.js b/mission/chapter03/mission_ch03/src/styled/MovieDetail.styled.js new file mode 100644 index 0000000..458bbd8 --- /dev/null +++ b/mission/chapter03/mission_ch03/src/styled/MovieDetail.styled.js @@ -0,0 +1,26 @@ +import styled from "styled-components"; + +export const MovieDetailContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + color: #fff; +`; + +export const MoviePoster = styled.img` + width: 300px; + border-radius: 10px; + margin-bottom: 20px; +`; + +export const MovieTitle = styled.h1` + font-size: 2rem; + margin: 10px 0; +`; + +export const MovieOverview = styled.p` + font-size: 1.2rem; + text-align: center; + max-width: 800px; +`; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/styled/RootLayout.styled.js b/mission/chapter03/mission_ch03/src/styled/RootLayout.styled.js new file mode 100644 index 0000000..4005801 --- /dev/null +++ b/mission/chapter03/mission_ch03/src/styled/RootLayout.styled.js @@ -0,0 +1,10 @@ +import styled from "styled-components"; + +export const LayoutContainer = styled.div` + display: flex; +`; + +export const MainContent = styled.div` + flex: 1; /* 사이드바를 제외한 나머지 공간을 차지 */ + margin-left: 15%; +`; \ No newline at end of file diff --git a/mission/chapter03/mission_ch03/src/styled/movie.styled.js b/mission/chapter03/mission_ch03/src/styled/movie.styled.js index 3b00056..b7d18bb 100644 --- a/mission/chapter03/mission_ch03/src/styled/movie.styled.js +++ b/mission/chapter03/mission_ch03/src/styled/movie.styled.js @@ -27,18 +27,24 @@ export const MovieCard = styled.div` background-color: #282828; border-radius: 10px; overflow: hidden; - width: 23%; + width: 240px; + height: 120px; cursor: pointer; transition: transform 0.3s ease; &:hover { transform: scale(1.05); } + + &:hover > div { + display: block; /* MovieCard에 커서가 올라가면 자식 div (MovieLabel) 화면에 표시 */ + } `; export const MovieImage = styled.img` width: 100%; - height: auto; + height: 100%; + object-fit: cover; // 이미지가 지정된 영역을 꽉 채우도록 설정 `; export const MovieLabel = styled.div` @@ -49,6 +55,7 @@ export const MovieLabel = styled.div` color: #fff; padding: 5px 10px; border-radius: 5px; + display: none; /* 초기 설정은 항상 화면에 안 보이게 함 */ `; export const MovieTitle = styled.h3` diff --git a/mission/chapter03/mission_ch03/src/styled/navbar.styled.js b/mission/chapter03/mission_ch03/src/styled/navbar.styled.js index eba00b5..f8f8bf9 100644 --- a/mission/chapter03/mission_ch03/src/styled/navbar.styled.js +++ b/mission/chapter03/mission_ch03/src/styled/navbar.styled.js @@ -9,6 +9,7 @@ export const NavbarContainer = styled.nav` padding: 10px 20px; `; +// YONGCHA 로고 export const Logo = styled(Link)` font-size: 24px; font-weight: bold; diff --git a/mission/chapter03/mission_ch03/src/styled/sidebar.styled.js b/mission/chapter03/mission_ch03/src/styled/sidebar.styled.js index 6766fe6..ac8a6ac 100644 --- a/mission/chapter03/mission_ch03/src/styled/sidebar.styled.js +++ b/mission/chapter03/mission_ch03/src/styled/sidebar.styled.js @@ -4,9 +4,9 @@ import { Link } from "react-router-dom"; export const SidebarContainer = styled.div` position: fixed; left: 0; - top: 0; - width: 200px; - height: 100%; + top: 60px; + width: 15%; + height: calc(100% - 60px); /* Navbar 높이를 제외한 남은 공간 */ background-color: #1b1b1b; display: flex; flex-direction: column; @@ -18,12 +18,9 @@ export const SidebarLink = styled(Link)` width: 100%; padding: 15px; color: white; - text-align: center; + text-align: left; + text-indent: 15px; text-decoration: none; - font-size: 18px; + font-size: 14px; transition: background-color 0.3s ease; - - &:hover { - background-color: #f04a7e; - } `; \ No newline at end of file