From 5572bb13e20a5a7b0b9c9d468933b5d29e4fe8e7 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 11:13:49 -0600 Subject: [PATCH 1/7] light styling, added searchbar component --- src/App.css | 10 +++++++++- src/features/navbar/Navbar.css | 0 src/features/navbar/Navbar.js | 4 +++- src/features/searchBar/searchBar.js | 30 +++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/features/navbar/Navbar.css diff --git a/src/App.css b/src/App.css index 5730ebf..c439ce4 100644 --- a/src/App.css +++ b/src/App.css @@ -1,5 +1,7 @@ .App { - text-align: center; + display: flex; + flex-direction: column; + align-items: center; margin: 0; padding: 0; } @@ -9,4 +11,10 @@ flex-direction: row; justify-content: space-between; align-items: baseline; + width: 100%; + border-bottom: 1px solid black; +} + +.navbar > * { + padding: 0 2rem; } \ No newline at end of file diff --git a/src/features/navbar/Navbar.css b/src/features/navbar/Navbar.css new file mode 100644 index 0000000..e69de29 diff --git a/src/features/navbar/Navbar.js b/src/features/navbar/Navbar.js index 2990440..ac3b552 100644 --- a/src/features/navbar/Navbar.js +++ b/src/features/navbar/Navbar.js @@ -1,10 +1,12 @@ import React from "react"; +import SearchBar from "../searchBar/searchBar"; +import './Navbar.css'; export default function Navbar() { return (

Reddit but it's all cats

-

Search bar here

+

Expand sidebar here

) diff --git a/src/features/searchBar/searchBar.js b/src/features/searchBar/searchBar.js index e69de29..68d2b6d 100644 --- a/src/features/searchBar/searchBar.js +++ b/src/features/searchBar/searchBar.js @@ -0,0 +1,30 @@ +import React, { useState, useEffect } from "react"; + +export default function SearchBar() { + const [term, setTerm] = useState(''); + + const handleChange = (e) => { + e.preventDefault(); + setTerm(e.target.value); + } + + useEffect(() => { + let isSearching = true; + + if (term) { + // dispatch an action which filters content by {term} + } else { + return; + } + + return () => { + isSearching = false; + } + }, [term]) + + return ( + <> + + + ); +} \ No newline at end of file From a67eddd35283ed3c5f9c0b84e4831d46eed6e143 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 11:25:51 -0600 Subject: [PATCH 2/7] more light styling --- src/App.css | 18 ++++++++++++++++++ src/App.js | 21 +++++++++++++++++++++ src/features/posts/Post.css | 21 +++++++++++++++++++++ src/features/posts/Post.js | 18 ++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 src/features/posts/Post.css diff --git a/src/App.css b/src/App.css index c439ce4..80ee5cd 100644 --- a/src/App.css +++ b/src/App.css @@ -17,4 +17,22 @@ .navbar > * { padding: 0 2rem; +} + +.content-container { + display: inline-flex; + flex-direction: row; +} + +.feed { + display: inline-flex; + flex-direction: column; + align-items: center; + width: 90vw; +} + +.about-the-app { + display: hidden; + flex-direction: column; + align-items: center; } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 99c00b2..6d37811 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,7 @@ import React from 'react'; import './App.css'; import Navbar from './features/navbar/Navbar'; +import Post from './features/posts/Post'; import redditSlice from './features/reddit/redditSlice'; function App() { @@ -8,6 +9,26 @@ function App() {

Stuff

+
+ +
+ {/* To do: import posts from post directory */} + {/* Map post data onto individual post cards, handle undefined values */} + + + + + +
+ +
+ {/* To do: add mutable state to class name for this div, */} + {/* determining whether or not it's active based on the state of */} + {/* The action dispatched from the searchbar slice(?) */} + {/* Do I need a searchbar slice? */} +
+ +
); } diff --git a/src/features/posts/Post.css b/src/features/posts/Post.css new file mode 100644 index 0000000..7a5ed35 --- /dev/null +++ b/src/features/posts/Post.css @@ -0,0 +1,21 @@ +.post-body { + display: inline-flex; + flex-direction: column; + width: 75%; + padding: 2rem; + border: 1px solid black; + margin: 3rem 0; +} + +.image-placeholder { + display: inline-flex; + background-color: grey; + width: 30%; + height: 15rem; +} + +.post-metadata { + display: inline-flex; + flex-direction: row; + justify-content: space-between; +} \ No newline at end of file diff --git a/src/features/posts/Post.js b/src/features/posts/Post.js index e69de29..f406cdc 100644 --- a/src/features/posts/Post.js +++ b/src/features/posts/Post.js @@ -0,0 +1,18 @@ +import React from "react"; +import './Post.css'; + +export default function Post() { + return ( + <> +
+

Post title

+
+
+

u/username

+

posted at midnight

+

a million comments

+
+
+ + ); +} \ No newline at end of file From 68ec1ca06132427bd163674ee05b0a965a304a8d Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 12:02:17 -0600 Subject: [PATCH 3/7] sidebar with hardcoded values --- src/App.css | 27 +++++++++++++++++++++++++++ src/features/navbar/Navbar.css | 0 src/features/navbar/Navbar.js | 17 ++++++++++++++--- src/features/searchBar/searchBar.js | 6 ------ src/features/sidebar/Sidebar.js | 12 ++++++++++++ 5 files changed, 53 insertions(+), 9 deletions(-) delete mode 100644 src/features/navbar/Navbar.css diff --git a/src/App.css b/src/App.css index 80ee5cd..7b201a4 100644 --- a/src/App.css +++ b/src/App.css @@ -12,6 +12,7 @@ justify-content: space-between; align-items: baseline; width: 100%; + height: 5rem; border-bottom: 1px solid black; } @@ -35,4 +36,30 @@ display: hidden; flex-direction: column; align-items: center; +} + +.sidebar { + display: flex; + flex-direction: column; + width: 12rem; + position: fixed; + background-color: black; + color: white; + right: 0; + top: 5rem; + padding: 1.5rem; + transition: right 0.6s ease-out; +} + +.sidebar-hidden { + display: flex; + flex-direction: column; + width: 12rem; + position: fixed; + background-color: black; + color: white; + right: -15rem; + top: 5rem; + padding: 1.5rem; + transition: right 0.6s ease-out; } \ No newline at end of file diff --git a/src/features/navbar/Navbar.css b/src/features/navbar/Navbar.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/features/navbar/Navbar.js b/src/features/navbar/Navbar.js index ac3b552..9066a9d 100644 --- a/src/features/navbar/Navbar.js +++ b/src/features/navbar/Navbar.js @@ -1,13 +1,24 @@ -import React from "react"; +import React, { useState } from "react"; import SearchBar from "../searchBar/searchBar"; -import './Navbar.css'; +import Sidebar from "../sidebar/Sidebar"; export default function Navbar() { + const [collapsed, setCollapsed] = useState(true); + + const handleCollapse = () => { + setCollapsed(!collapsed); + } + return ( + <>

Reddit but it's all cats

-

Expand sidebar here

+
+
+ +
+ ) } \ No newline at end of file diff --git a/src/features/searchBar/searchBar.js b/src/features/searchBar/searchBar.js index 68d2b6d..81d497a 100644 --- a/src/features/searchBar/searchBar.js +++ b/src/features/searchBar/searchBar.js @@ -9,17 +9,11 @@ export default function SearchBar() { } useEffect(() => { - let isSearching = true; - if (term) { // dispatch an action which filters content by {term} } else { return; } - - return () => { - isSearching = false; - } }, [term]) return ( diff --git a/src/features/sidebar/Sidebar.js b/src/features/sidebar/Sidebar.js index e69de29..37aca36 100644 --- a/src/features/sidebar/Sidebar.js +++ b/src/features/sidebar/Sidebar.js @@ -0,0 +1,12 @@ +import React from "react"; + +export default function Sidebar({isCollapsed}) { + return ( +
+

Hard coded subreddit

+

Hard coded subreddit

+

Hard coded subreddit

+

Hard coded subreddit

+
+ ); +} \ No newline at end of file From cfe92667a128693280adb7c116edcfcd6ec143b2 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 12:27:08 -0600 Subject: [PATCH 4/7] experimenting with fetching posts --- package-lock.json | 3 ++ package.json | 3 ++ src/App.js | 7 +--- src/features/posts/Feed.js | 61 ++++++++++++++++++++++++++++++ src/features/posts/Post.js | 3 +- src/features/posts/postsSlice.js | 9 ++++- src/features/reddit/redditSlice.js | 1 + 7 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/features/posts/Feed.js diff --git a/package-lock.json b/package-lock.json index 969c081..b14bb7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,9 @@ "react-dom": "^17.0.2", "react-redux": "^7.2.6", "react-scripts": "5.0.0" + }, + "devDependencies": { + "uuid": "^8.3.2" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 4ab844f..7bd4505 100644 --- a/package.json +++ b/package.json @@ -32,5 +32,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "uuid": "^8.3.2" } } diff --git a/src/App.js b/src/App.js index 6d37811..fa3593e 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,7 @@ import React from 'react'; import './App.css'; import Navbar from './features/navbar/Navbar'; import Post from './features/posts/Post'; -import redditSlice from './features/reddit/redditSlice'; +import Feed from './features/posts/Feed'; function App() { return ( @@ -14,10 +14,7 @@ function App() {
{/* To do: import posts from post directory */} {/* Map post data onto individual post cards, handle undefined values */} - - - - +
diff --git a/src/features/posts/Feed.js b/src/features/posts/Feed.js new file mode 100644 index 0000000..859b749 --- /dev/null +++ b/src/features/posts/Feed.js @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { fetchBySub, updatePosts, selectPosts } from "./postsSlice"; +import { selectAllSubs } from "../reddit/redditSlice"; +import { useSelector, useDispatch } from "react-redux"; +import { v4 } from "uuid"; +import Post from "./Post"; + +export default function Feed() { + const [feed, setFeed] = useState(null); + const dispatch = useDispatch(); + const allSubs = useSelector(selectAllSubs); + + const doTheThing = async() => { + try { + let myPromises = []; + for (let sub in allSubs) { + myPromises.push(dispatch(fetchBySub(sub))); + } + let response = await Promise.all([...myPromises]).then((response) => dispatch(updatePosts(response))); + console.log(response); + } catch(e) { + console.log(e); + } + } + + doTheThing(); + + const allPosts = useSelector(selectPosts); + + const handlePosts = () => { + if (allPosts) { + try { + allPosts.forEach((post) => { + setFeed((prev) => [ + ...prev, + + ]) + }) + } catch(e) { + console.log(e); + } + } + } + + handlePosts(); + + return ( +
+ {feed} +
+ ); +} \ No newline at end of file diff --git a/src/features/posts/Post.js b/src/features/posts/Post.js index f406cdc..392f843 100644 --- a/src/features/posts/Post.js +++ b/src/features/posts/Post.js @@ -1,7 +1,8 @@ import React from "react"; import './Post.css'; -export default function Post() { +export default function Post({props}) { + return ( <>
diff --git a/src/features/posts/postsSlice.js b/src/features/posts/postsSlice.js index 9413cee..46c2d2c 100644 --- a/src/features/posts/postsSlice.js +++ b/src/features/posts/postsSlice.js @@ -25,7 +25,10 @@ export const postsSlice = createSlice({ reducers: { filterPosts(state,action) { // Expects action.payload to be the searchterm imported from the state of searchBar state.posts.filter(post => (post.data.title !== action.payload) && (post.data.selftext !== action.payload)); - } + }, + updatePosts(state,action) { + state.posts = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(fetchBySub.pending, (state,action) => { @@ -47,4 +50,6 @@ export const postsSlice = createSlice({ }); export default postsSlice.reducer; -export const { filterPosts } = postsSlice.actions; \ No newline at end of file +export const selectPosts = state => state.posts; +export const { filterPosts, updatePosts } = postsSlice.actions; +// exports also includes fetchBySub (takes argument of a sub) \ No newline at end of file diff --git a/src/features/reddit/redditSlice.js b/src/features/reddit/redditSlice.js index ae2524f..5184af4 100644 --- a/src/features/reddit/redditSlice.js +++ b/src/features/reddit/redditSlice.js @@ -73,4 +73,5 @@ export const redditSlice = createSlice({ }); export default redditSlice.reducer; +export const selectAllSubs = state => state.subreddits; export const { updateSubVisibility } = redditSlice.actions; \ No newline at end of file From 3536df4b591edd7d812be446e42fe0d8dae4826f Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 13:14:46 -0600 Subject: [PATCH 5/7] data successfully mapped onto feed --- src/features/posts/Feed.js | 60 +++++++++++++++++--------------------- src/features/posts/Post.js | 12 ++++---- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/features/posts/Feed.js b/src/features/posts/Feed.js index 859b749..8f772c1 100644 --- a/src/features/posts/Feed.js +++ b/src/features/posts/Feed.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { fetchBySub, updatePosts, selectPosts } from "./postsSlice"; import { selectAllSubs } from "../reddit/redditSlice"; import { useSelector, useDispatch } from "react-redux"; @@ -8,31 +8,18 @@ import Post from "./Post"; export default function Feed() { const [feed, setFeed] = useState(null); const dispatch = useDispatch(); - const allSubs = useSelector(selectAllSubs); - const doTheThing = async() => { - try { - let myPromises = []; - for (let sub in allSubs) { - myPromises.push(dispatch(fetchBySub(sub))); - } - let response = await Promise.all([...myPromises]).then((response) => dispatch(updatePosts(response))); - console.log(response); - } catch(e) { - console.log(e); - } - } + useEffect(() => { + let isActive = true; - doTheThing(); + const getPosts = async() => { + let myPosts = await dispatch(fetchBySub('https://www.reddit.com/r/cats.json')); + myPosts = myPosts.payload; - const allPosts = useSelector(selectPosts); - - const handlePosts = () => { - if (allPosts) { - try { - allPosts.forEach((post) => { - setFeed((prev) => [ - ...prev, + if (typeof myPosts === 'object' && isActive) { + let newFeed = []; + for (let post of myPosts) { + newFeed.push( - ]) - }) - } catch(e) { - console.log(e); + ); + } + setFeed(newFeed); } - } - } + }; + getPosts(); - handlePosts(); + return () => { + isActive = false; + } + + }, [dispatch]) + + useEffect(() => { + console.log(feed); + }, [feed]) return ( -
- {feed} -
+ <> + {feed} + ); } \ No newline at end of file diff --git a/src/features/posts/Post.js b/src/features/posts/Post.js index 392f843..0210f43 100644 --- a/src/features/posts/Post.js +++ b/src/features/posts/Post.js @@ -1,17 +1,17 @@ import React from "react"; import './Post.css'; -export default function Post({props}) { +export default function Post({title,author,subreddit,ups,comments,time,id,media}) { return ( <>
-

Post title

-
+

{title ? title : 'title'}

+ {title}
-

u/username

-

posted at midnight

-

a million comments

+

{author ? author : 'u/username'}

+

{time ? time : ''}

+

{comments ? comments : 'comments'}

From c07cda263cdbdb8ba11e4d34d1b208765ae63a81 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 13:38:50 -0600 Subject: [PATCH 6/7] several media types now accounted for --- src/App.js | 2 -- src/features/posts/Feed.js | 12 ++++++------ src/features/posts/Post.css | 9 +++++++++ src/features/posts/Post.js | 22 +++++++++++++++++----- src/features/posts/notepad.txt | 9 +++++++++ 5 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 src/features/posts/notepad.txt diff --git a/src/App.js b/src/App.js index fa3593e..d081c13 100644 --- a/src/App.js +++ b/src/App.js @@ -8,14 +8,12 @@ function App() { return (
-

Stuff

{/* To do: import posts from post directory */} {/* Map post data onto individual post cards, handle undefined values */} -
diff --git a/src/features/posts/Feed.js b/src/features/posts/Feed.js index 8f772c1..d5e878d 100644 --- a/src/features/posts/Feed.js +++ b/src/features/posts/Feed.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { fetchBySub, updatePosts, selectPosts } from "./postsSlice"; +import postsSlice, { fetchBySub, updatePosts, selectPosts } from "./postsSlice"; import { selectAllSubs } from "../reddit/redditSlice"; import { useSelector, useDispatch } from "react-redux"; import { v4 } from "uuid"; @@ -15,6 +15,7 @@ export default function Feed() { const getPosts = async() => { let myPosts = await dispatch(fetchBySub('https://www.reddit.com/r/cats.json')); myPosts = myPosts.payload; + console.log(myPosts); if (typeof myPosts === 'object' && isActive) { let newFeed = []; @@ -29,6 +30,9 @@ export default function Feed() { time={post.data.created_utc} id={v4()} media={post.data.post_hint === 'image' && post.data.url} + permalink={post.data.permalink} + selftext={post.data.selftext} + video={post.data.is_video ? post.data.media.reddit_video.fallback_url : null} /> ); } @@ -43,13 +47,9 @@ export default function Feed() { }, [dispatch]) - useEffect(() => { - console.log(feed); - }, [feed]) - return ( <> - {feed} + {feed ? feed :

Loading cats for you...

} ); } \ No newline at end of file diff --git a/src/features/posts/Post.css b/src/features/posts/Post.css index 7a5ed35..7577f35 100644 --- a/src/features/posts/Post.css +++ b/src/features/posts/Post.css @@ -14,6 +14,15 @@ height: 15rem; } +a { + font-size: 2rem; +} + +img, video { + max-height: 45rem; + object-fit: contain; +} + .post-metadata { display: inline-flex; flex-direction: row; diff --git a/src/features/posts/Post.js b/src/features/posts/Post.js index 0210f43..bc65e4a 100644 --- a/src/features/posts/Post.js +++ b/src/features/posts/Post.js @@ -1,15 +1,27 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import './Post.css'; -export default function Post({title,author,subreddit,ups,comments,time,id,media}) { +export default function Post({title,author,subreddit,ups,comments,time,id,media,permalink,selftext,video}) { + const limit = 300; + const [body, setBody] = useState(selftext); + + useEffect(() => { + if (selftext.length > limit) { + setBody(selftext.substring(0,limit) + '...'); + } else { + return; + } + }) return ( <>
-

{title ? title : 'title'}

- {title} + {title ? title : 'title'} + {media ? {title} : ''} + {video ? : ''} +

{body}

-

{author ? author : 'u/username'}

+

{author ? 'u/' + author : 'u/username'}

{time ? time : ''}

{comments ? comments : 'comments'}

diff --git a/src/features/posts/notepad.txt b/src/features/posts/notepad.txt new file mode 100644 index 0000000..79956f4 --- /dev/null +++ b/src/features/posts/notepad.txt @@ -0,0 +1,9 @@ +bitrate_kbps: 1200 +dash_url: "https://v.redd.it/5vx109l7x8e81/DASHPlaylist.mpd?a=1645903634%2CZThiNjM5ZTc5MmQ3ZmIxYzg4Y2YzYzc2MDk2MjgzNzU0N2RlOTZjMzhmNWFhN2VjNDI2OWZmNWI0MDY1NDQxOA%3D%3D&v=1&f=hd" +duration: 23 +fallback_url: "https://v.redd.it/5vx109l7x8e81/DASH_480.mp4?source=fallback" +height: 480 +hls_url: "https://v.redd.it/5vx109l7x8e81/HLSPlaylist.m3u8?a=1645903634%2COWI5Nzc2MzM0ZTExNTVlMWZiODBhMGJiMjMxYjU4NDk3ZDcwMTEzOGEyN2M4MDA3MmE0YWFlNzMzOTg0MjIzNw%3D%3D&v=1&f=hd" +is_gif: false +scrubber_media_url: "https://v.redd.it/5vx109l7x8e81/DASH_96.mp4" +transcoding_status: "completed" \ No newline at end of file From 40cc50d8c7e1f47093cd78c1dfda487546ea6d33 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Thu, 27 Jan 2022 14:19:58 -0600 Subject: [PATCH 7/7] syntax corrected for selectors --- src/features/posts/notepad.txt | 9 --------- src/features/posts/postsSlice.js | 4 ++-- src/features/reddit/redditSlice.js | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 src/features/posts/notepad.txt diff --git a/src/features/posts/notepad.txt b/src/features/posts/notepad.txt deleted file mode 100644 index 79956f4..0000000 --- a/src/features/posts/notepad.txt +++ /dev/null @@ -1,9 +0,0 @@ -bitrate_kbps: 1200 -dash_url: "https://v.redd.it/5vx109l7x8e81/DASHPlaylist.mpd?a=1645903634%2CZThiNjM5ZTc5MmQ3ZmIxYzg4Y2YzYzc2MDk2MjgzNzU0N2RlOTZjMzhmNWFhN2VjNDI2OWZmNWI0MDY1NDQxOA%3D%3D&v=1&f=hd" -duration: 23 -fallback_url: "https://v.redd.it/5vx109l7x8e81/DASH_480.mp4?source=fallback" -height: 480 -hls_url: "https://v.redd.it/5vx109l7x8e81/HLSPlaylist.m3u8?a=1645903634%2COWI5Nzc2MzM0ZTExNTVlMWZiODBhMGJiMjMxYjU4NDk3ZDcwMTEzOGEyN2M4MDA3MmE0YWFlNzMzOTg0MjIzNw%3D%3D&v=1&f=hd" -is_gif: false -scrubber_media_url: "https://v.redd.it/5vx109l7x8e81/DASH_96.mp4" -transcoding_status: "completed" \ No newline at end of file diff --git a/src/features/posts/postsSlice.js b/src/features/posts/postsSlice.js index 46c2d2c..7c491a8 100644 --- a/src/features/posts/postsSlice.js +++ b/src/features/posts/postsSlice.js @@ -1,7 +1,7 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; export const fetchBySub = createAsyncThunk( - 'reddit/fetchBySub', + 'posts/fetchBySub', async(subreddit) => { // expects an argument corresponding to the url, in json format, of a given subreddit try { const myRequest = new Request(subreddit); // initializes request @@ -50,6 +50,6 @@ export const postsSlice = createSlice({ }); export default postsSlice.reducer; -export const selectPosts = state => state.posts; +export const selectPosts = state => state.postsSlice.posts; export const { filterPosts, updatePosts } = postsSlice.actions; // exports also includes fetchBySub (takes argument of a sub) \ No newline at end of file diff --git a/src/features/reddit/redditSlice.js b/src/features/reddit/redditSlice.js index 5184af4..537677d 100644 --- a/src/features/reddit/redditSlice.js +++ b/src/features/reddit/redditSlice.js @@ -73,5 +73,5 @@ export const redditSlice = createSlice({ }); export default redditSlice.reducer; -export const selectAllSubs = state => state.subreddits; +export const selectAllSubs = state => state.redditSlice.subreddits; export const { updateSubVisibility } = redditSlice.actions; \ No newline at end of file